一行代碼實現一個簡單的模板字符串替換

起始

同許多初學 Javascript 的菜鳥同樣,起初,我也是採用拼接字符串的形式,將 JSON 數據嵌入 HTML 中。開始時代碼量較少,暫時還能夠接受。但當頁面結構複雜起來後,其弱點開始變得沒法忍受起來:html

  • 書寫不連貫。每寫一個變量就要斷一下,插入一個 + 和 "。十分容易出錯。
  • 沒法重用。HTML 片斷都是離散化的數據,難以對其中重複的部分進行提取。
  • 沒法很好地利用 <template> 標籤。這是 HTML5 中新增的一個標籤,標準極力推薦將 HTML 模板放入 <template> 標籤中,使代碼更簡潔。

當時個人心情就是這樣的:
這TMD是在逗我嗎。html5

因而出來了後來的 ES6ES6的模板字符串用起來着實方便,對於比較老的項目,項目沒webpackgulp 等構建工具,沒法使用 ES6 的語法,可是想也借鑑這種優秀的處理字符串拼接的方式,咱們不妨能夠試着本身寫一個,主要是思路,可使用 ES6 語法模擬 ES6的模板字符串的這個功能。webpack

後端返回的通常都是 JSON 的數據格式,因此咱們按照下面的規則進行模擬。git

需求描述

實現一個 render(template, context) 方法,將 template 中的佔位符用 context 填充。

要求:

不須要有控制流成分(如 循環、條件 等等),只要有變量替換功能便可
級聯的變量也能夠展開
被轉義的的分隔符 { 和 } 不該該被渲染,分隔符與變量之間容許有空白字符
var obj = {name:"二月",age:"15"};
var str = "{{name}}很厲害,才{{age}}歲";
輸出:二月很厲害,才15歲。

PS:本文須要對正則表達式有必定的瞭解,若是還不瞭解正則表達式,建議先去學習一下,正則也是面試筆試必備的技能,上面連接末尾有很多正則學習的連接。es6

若是是你,你會怎麼實現?能夠先嚐試本身寫寫,實現也不難。github

先不說個人實現,我把這個題給其餘好友作的時候,實現的不盡相同,咱們先看幾位童鞋的實現,而後在他們的基礎上找到常見的誤區以及實現不夠優雅的地方。web

二月童鞋:

let str = "{{name}}很厲害,才{{age}}歲"
let obj = {name: '二月', age: 15}
function test(str, obj){
    let _s = str.replace(/\{\{(\w+)\}\}/g, '$1')
    let result
    for(let k in obj) {
      _s = _s.replace(new RegExp(k, 'g'), obj[k])
    }
  return _s
}
const s = test(str, obj)

最基本的是實現了,可是代碼仍是有不少問題沒考慮到,首先 Object 的 key 值不必定只是 w,
還有就是若是字符串是這種的:面試

let str = "{{name}}很name厲害,才{{age}}歲"`
會輸出 :二月很厲害二月害,才15歲

此處你須要瞭解正則的分組纔會明白 $1 的含義,錯誤很明顯,把原本就是字符串不要替換的 name 也給替換了,從代碼咱們能夠看出二月的思路。正則表達式

  1. 代碼的做用目標是 str,先用正則匹配出 {{name}}{{age}},而後用分組獲取括號的 name,age,最後用 replace 方法把 {{name}}{{age}} 替換成 nameage,最後字符串就成了 name很name厲害,才age歲,最後 for in 循環的時候才致使一塊兒都被替換掉了。
  2. for in 循環徹底不必,能不用 for in 儘可能不要用 for infor in 會遍歷自身以及原型鏈全部的屬性。

志欽童鞋:

var str = "{{name}}很厲害,才{{age}}歲";
var str2 = "{{name}}很厲name害,才{{age}}歲{{name}}";

var obj = {name: '周杰倫', age: 15};
function fun(str, obj) {
    var arr;
    arr = str.match(/{{[a-zA-Z\d]+}}/g);
    for(var i=0;i<arr.length;i++){
        arr[i] = arr[i].replace(/{{|}}/g,'');
        str = str.replace('{{'+arr[i]+'}}',obj[arr[i]]);
    }
    return str;
}
console.log(fun(str,obj));
console.log(fun(str2,obj));

思路是正確的,知道最後要替換的是 {{name}}{{age}} 總體,而不是像二月童鞋那樣最後去替換 name,全部跑起來確定沒問題,實現是實現了可是感受有點那個,咱們要探討的是一行代碼也就是代碼越少越好。json

小維童鞋:

function a(str, obj) {
  var str1 = str;
  for (var key in obj) {
    var re = new RegExp("{{" + key + "}}", "g");
    str1 = str1.replace(re, obj[key]);
  }
  console.log(str1);
}
const str = "{{name}}很厲name害{{name}},才{{age}}歲";
const obj = { name: "jawil", age: "15" };
a(str, obj);

實現的已經簡單明瞭了,就是把 objkey 值遍歷,而後拼成 {{key}},最後用 obj[key] 也就是 value{{key}} 整個給替換了,思路很好,跟我最初的版本一個樣。

個人實現:

function parseString(str, obj) {
  Object.keys(obj).forEach(key => {
    str = str.replace(new RegExp(`{{${key}}}`,'g'), obj[key]);
  });
  return str;
}
const str = "{{name}}很厲name害{{name}},才{{age}}歲";
const obj = { name: "jawil", age: "15" };
console.log(parseString(str, obj));

其實這裏仍是有些問題的,首先我沒用 for...in 循環就是爲了考慮沒必要要的循環,由於 for...in 循環會遍歷原型鏈全部的可枚舉屬性,形成沒必要要的循環。

咱們能夠簡單看一個例子,看看 for...in的可怕性。

// Chrome v63
const div = document.createElement('div');
let m = 0;
for (let k in div) {
  m++;
}
let n = 0;
console.log(m); // 231
console.log(Object.keys(div).length); // 0

一個 DOM 節點屬性居然有這麼多的屬性,列舉這個例子只是讓你們看到 for in 遍歷的效率問題,不要輕易用 for in循環,經過這個 DOM 節點之多也能夠必定程度瞭解到 React Virtual DOM 的思想和優越性。

除了用 for in 循環獲取 objkey 值,還能夠用 Object.key() 獲取,Object.getOwnPropertyNames() 以及 Reflect.ownKeys()也能夠獲取,那麼這幾種有啥區別呢?這裏就簡單說一下他們的一些區別。

for...in循環:會遍歷對象自身的屬性,以及原型屬性, for...in 循環只遍歷可枚舉(不包括 enumerablefalse )屬性。像 ArrayObject 使用內置構造函數所建立的對象都會繼承自 Object.prototypeString.prototype 的不可枚舉屬性;

Object.key():能夠獲得自身可枚舉的屬性,但得不到原型鏈上的屬性;

Object.getOwnPropertyNames():能夠獲得自身全部的屬性(包括不可枚舉),但得不到原型鏈上的屬性, Symbols 屬性也得不到.

Reflect.ownKeys:該方法用於返回對象的全部屬性,基本等同於 Object.getOwnPropertyNames()Object.getOwnPropertySymbols 之和。

上面說的可能比較抽象,不夠直觀。能夠看個我寫的 DEMO,一切簡單明鳥。

const parent = {
  a: 1,
  b: 2,
  c: 3
};
const child = {
  d: 4,
  e: 5,
  [Symbol()]: 6
};
child.__proto__ = parent;
Object.defineProperty(child, "d", { enumerable: false });

for (var attr in child) {
  console.log("for...in:", attr);// a,b,c,e
}
console.log("Object.keys:", Object.keys(child));// [ 'e' ]
console.log("Object.getOwnPropertyNames:", Object.getOwnPropertyNames(child)); // [ 'd', 'e' ]
console.log("Reflect.ownKeys:", Reflect.ownKeys(child)); //  [ 'd', 'e', Symbol() ]

最後實現

上面的實現其實已經很簡潔了,可是仍是有些不完美的地方,經過 MDN 首先咱們先了解一下 replace 的用法。

經過文檔裏面寫的 str.replace(regexp|substr, newSubStr|function) ,咱們能夠發現 replace 方法能夠傳入 function 回調函數,

function (replacement) 一個用來建立新子字符串的函數,該函數的返回值將替換掉第一個參數匹配到的結果。參考這個指定一個函數做爲參數

有了這句話,其實就很好實現了,先看看具體代碼再作下一步分析。

function render(template, context) {
  return template.replace(/\{\{(.*?)\}\}/g, (match, key) => context[key]);
}
const template = "{{name}}很厲name害,才{{age}}歲";
const context = { name: "jawil", age: "15" };
console.log(render(template, context));

能夠對照上面文檔的話來作分析:該函數的返回值(obj[key]=jawil)將替換掉第一個參數(match=={{name}})匹配到的結果。

簡單分析一下:.*? 是正則固定搭配用法,表示非貪婪匹配模式,儘量匹配少的,什麼意思呢?舉個簡單的例子。

先看一個例子:

源字符串:aa<div>test1</div>bb<div>test2</div>cc

正則表達式一:<div>.*</div>

匹配結果一:<div>test1</div>bb<div>test2</div>

正則表達式二:<div>.*?</div>

匹配結果二:<div>test1</div>(這裏指的是一次匹配結果,不使用/g,因此沒包括<div>test2</div>)

根據上面的例子,從匹配行爲上分析一下,什是貪婪與非貪婪匹配模式。

利用非貪婪匹配模就能匹配到全部的{{name}}{{age}},上面的也說到過正則分組,分組匹配到的就是 name,也就是 function 的第二個參數 key

因此這行代碼的意思就很清楚,正則匹配到{{name}},分組獲取 name,而後把 {{name}} 替換成 obj[name](jawil)

固然後來發現還有一個小問題,若是有空格的話就會匹配失敗,相似這種寫法:

const template = "{{name   }}很厲name害,才{{age   }}歲";

因此在上面的基礎上還要去掉空格,其實也很簡單,用正則或者 String.prototype.trim() 方法都行。

function render(template, context) {
  return template.replace(/\{\{(.*?)\}\}/g, (match, key) => context[key.trim()]);
}
const template = "{{name   }}很厲name害,才{{age   }}歲";
const context = { name: "jawil", age: "15" };
console.log(render(template, context));

將函數掛到 String 的原型鏈,獲得最終版本

甚至,咱們能夠經過修改原型鏈,實現一些很酷的效果:

String.prototype.render = function (context) {
  return this.replace(/\{\{(.*?)\}\}/g, (match, key) => context[key.trim()]);
};

若是{}中間不是數字,則{}自己不須要轉義,因此最終最簡潔的代碼:

String.prototype.render = function (context) {
  return this.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()]);
};

以後,咱們即可以這樣調用啦:

"{{name}}很厲name害,才{{ age  }}歲".render({ name: "jawil", age: "15" });

收穫

經過一個小小的模板字符串的實現,領悟到要把一個功能實現不難,要把作到完美真是難上加難,須要對基礎掌握牢固,有必定的沉澱,而後不斷地打磨才能比較優雅的實現,經過由一個很小的點每每能夠拓展出不少的知識點。

一張圖快速入門正則表達式:

相關文章
相關標籤/搜索