同許多初學 Javascript
的菜鳥同樣,起初,我也是採用拼接字符串的形式,將 JSON
數據嵌入 HTML
中。開始時代碼量較少,暫時還能夠接受。但當頁面結構複雜起來後,其弱點開始變得沒法忍受起來:html
HTML
片斷都是離散化的數據,難以對其中重複的部分進行提取。<template>
標籤。這是 HTML5
中新增的一個標籤,標準極力推薦將 HTML
模板放入 <template>
標籤中,使代碼更簡潔。當時個人心情就是這樣的:
這TMD是在逗我嗎。html5
因而出來了後來的 ES6
,ES6的模板字符串用起來着實方便,對於比較老的項目,項目沒webpack
,gulp
等構建工具,沒法使用 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
也給替換了,從代碼咱們能夠看出二月的思路。正則表達式
str
,先用正則匹配出 {{name}}
和 {{age}}
,而後用分組獲取括號的 name
,age
,最後用 replace
方法把 {{name}}
和 {{age}}
替換成 name
和 age
,最後字符串就成了 name很name厲害,才age歲,最後 for in
循環的時候才致使一塊兒都被替換掉了。for in
循環徹底不必,能不用 for in
儘可能不要用 for in
,for 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);
實現的已經簡單明瞭了,就是把 obj
的 key
值遍歷,而後拼成 {{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
循環獲取 obj
的 key
值,還能夠用 Object.key()
獲取,Object.getOwnPropertyNames()
以及 Reflect.ownKeys()
也能夠獲取,那麼這幾種有啥區別呢?這裏就簡單說一下他們的一些區別。
for...in
循環:會遍歷對象自身的屬性,以及原型屬性,for...in
循環只遍歷可枚舉(不包括enumerable
爲false
)屬性。像Array
和Object
使用內置構造函數所建立的對象都會繼承自Object.prototype
和String.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" });
經過一個小小的模板字符串的實現,領悟到要把一個功能實現不難,要把作到完美真是難上加難,須要對基礎掌握牢固,有必定的沉澱,而後不斷地打磨才能比較優雅的實現,經過由一個很小的點每每能夠拓展出不少的知識點。