起始
同許多初學(xué) Javascript
的菜鳥一樣私痹,起初淤击,我也是采用拼接字符串的形式匠抗,將 JSON
數(shù)據(jù)嵌入 HTML
中。開始時(shí)代碼量較少污抬,暫時(shí)還可以接受汞贸。但當(dāng)頁(yè)面結(jié)構(gòu)復(fù)雜起來(lái)后,其弱點(diǎn)開始變得無(wú)法忍受起來(lái):
- 書寫不連貫印机。每寫一個(gè)變量就要斷一下矢腻,插入一個(gè) + 和 "。十分容易出錯(cuò)射赛。
- 無(wú)法重用多柑。
HTML
片段都是離散化的數(shù)據(jù),難以對(duì)其中重復(fù)的部分進(jìn)行提取楣责。 - 無(wú)法很好地利用
<template>
標(biāo)簽竣灌。這是HTML5
中新增的一個(gè)標(biāo)簽,標(biāo)準(zhǔn)極力推薦將HTML
模板放入<template>
標(biāo)簽中秆麸,使代碼更簡(jiǎn)潔初嘹。
當(dāng)時(shí)我的心情就是這樣的:
這TMD是在逗我嗎。
于是出來(lái)了后來(lái)的 ES6
沮趣,ES6的模板字符串用起來(lái)著實(shí)方便屯烦,對(duì)于比較老的項(xiàng)目,項(xiàng)目沒(méi)webpack
兔毒,gulp
等構(gòu)建工具漫贞,無(wú)法使用 ES6
的語(yǔ)法,但是想也借鑒這種優(yōu)秀的處理字符串拼接的方式育叁,我們不妨可以試著自己寫一個(gè)迅脐,主要是思路,可以使用 ES6
語(yǔ)法模擬 ES6的模板字符串的這個(gè)功能豪嗽。
后端返回的一般都是 JSON
的數(shù)據(jù)格式谴蔑,所以我們按照下面的規(guī)則進(jìn)行模擬豌骏。
需求描述
實(shí)現(xiàn)一個(gè) render(template, context) 方法,將 template 中的占位符用 context 填充隐锭。
要求:
不需要有控制流成分(如 循環(huán)窃躲、條件 等等),只要有變量替換功能即可
級(jí)聯(lián)的變量也可以展開
被轉(zhuǎn)義的的分隔符 { 和 } 不應(yīng)該被渲染钦睡,分隔符與變量之間允許有空白字符
var obj = {name:"二月",age:"15"};
var str = "{{name}}很厲害蒂窒,才{{age}}歲";
輸出:二月很厲害,才15歲荞怒。
PS:本文需要對(duì)正則表達(dá)式有一定的了解洒琢,如果還不了解正則表達(dá)式,建議先去學(xué)習(xí)一下褐桌,正則也是面試筆試必備的技能衰抑,上面鏈接末尾有不少正則學(xué)習(xí)的鏈接。
如果是你荧嵌,你會(huì)怎么實(shí)現(xiàn)呛踊?可以先嘗試自己寫寫,實(shí)現(xiàn)也不難啦撮。
先不說(shuō)我的實(shí)現(xiàn)谭网,我把這個(gè)題給其他好友做的時(shí)候,實(shí)現(xiàn)的不盡相同赃春,我們先看幾位童鞋的實(shí)現(xiàn)蜻底,然后在他們的基礎(chǔ)上找到常見的誤區(qū)以及實(shí)現(xiàn)不夠優(yōu)雅的地方。
二月童鞋:
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)
最基本的是實(shí)現(xiàn)了,但是代碼還是有很多問(wèn)題沒(méi)考慮到要拂,首先 Object 的 key 值不一定只是 \w抠璃,
還有就是如果字符串是這種的:
let str = "{{name}}很name厲害,才{{age}}歲"`
會(huì)輸出 :二月很厲害二月害脱惰,才15歲
此處你需要了解正則的分組才會(huì)明白 $1
的含義搏嗡,錯(cuò)誤很明顯,把本來(lái)就是字符串不要替換的 name
也給替換了拉一,從代碼我們可以看出二月的思路采盒。
- 代碼的作用目標(biāo)是
str
,先用正則匹配出{{name}}
和{{age}}
蔚润,然后用分組獲取括號(hào)的name
,age
,最后用replace
方法把{{name}}
和{{age}}
替換成name
和age
磅氨,最后字符串就成了 name很name厲害,才age歲嫡纠,最后for in
循環(huán)的時(shí)候才導(dǎo)致一起都被替換掉了烦租。 - 用
for in
循環(huán)完全沒(méi)必要延赌,能不用for in
盡量不要用for in
,for in
會(huì)遍歷自身以及原型鏈所有的屬性叉橱。
志欽童鞋:
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
,所有跑起來(lái)肯定沒(méi)問(wèn)題粪小,實(shí)現(xiàn)是實(shí)現(xiàn)了但是感覺(jué)有點(diǎn)那個(gè)大磺,我們要探討的是一行代碼也就是代碼越少越好。
小維童鞋:
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);
實(shí)現(xiàn)的已經(jīng)簡(jiǎn)單明了了量没,就是把 obj
的 key
值遍歷,然后拼成 {{key}}
突想,最后用 obj[key]
也就是 value
把 {{key}}
整個(gè)給替換了殴蹄,思路很好,跟我最初的版本一個(gè)樣猾担。
我的實(shí)現(xiàn):
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));
其實(shí)這里還是有些問(wèn)題的,首先我沒(méi)用 for...in
循環(huán)就是為了考慮不必要的循環(huán)绑嘹,因?yàn)?for...in
循環(huán)會(huì)遍歷原型鏈所有的可枚舉屬性稽荧,造成不必要的循環(huán)。
我們可以簡(jiǎn)單看一個(gè)例子工腋,看看 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
一個(gè) DOM 節(jié)點(diǎn)屬性竟然有這么多的屬性,列舉這個(gè)例子只是讓大家看到 for in
遍歷的效率問(wèn)題擅腰,不要輕易用 for in
循環(huán)蟋恬,通過(guò)這個(gè) DOM
節(jié)點(diǎn)之多也可以一定程度了解到 React
的 Virtual DOM
的思想和優(yōu)越性。
除了用 for in
循環(huán)獲取 obj
的 key
值趁冈,還可以用 Object.key()
獲取歼争,Object.getOwnPropertyNames()
以及 Reflect.ownKeys()
也可以獲取,那么這幾種有啥區(qū)別呢渗勘?這里就簡(jiǎn)單說(shuō)一下他們的一些區(qū)別沐绒。
for...in
循環(huán):會(huì)遍歷對(duì)象自身的屬性,以及原型屬性旺坠,for...in
循環(huán)只遍歷可枚舉(不包括enumerable
為false
)屬性乔遮。像Array
和Object
使用內(nèi)置構(gòu)造函數(shù)所創(chuàng)建的對(duì)象都會(huì)繼承自Object.prototype
和String.prototype
的不可枚舉屬性;
Object.key()
:可以得到自身可枚舉的屬性,但得不到原型鏈上的屬性;
Object.getOwnPropertyNames()
:可以得到自身所有的屬性(包括不可枚舉),但得不到原型鏈上的屬性, Symbols 屬性也得不到.
Reflect.ownKeys
:該方法用于返回對(duì)象的所有屬性,基本等同于Object.getOwnPropertyNames()
與Object.getOwnPropertySymbols
之和取刃。
上面說(shuō)的可能比較抽象申眼,不夠直觀瞒津。可以看個(gè)我寫的 DEMO
括尸,一切簡(jiǎn)單明鳥巷蚪。
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() ]
最后實(shí)現(xiàn)
上面的實(shí)現(xiàn)其實(shí)已經(jīng)很簡(jiǎn)潔了,但是還是有些不完美的地方濒翻,通過(guò) MDN 首先我們先了解一下 replace 的用法屁柏。
通過(guò)文檔里面寫的 str.replace(regexp|substr, newSubStr|function)
,我們可以發(fā)現(xiàn) replace 方法可以傳入 function
回調(diào)函數(shù)有送,
function (replacement)
一個(gè)用來(lái)創(chuàng)建新子字符串的函數(shù)淌喻,該函數(shù)的返回值將替換掉第一個(gè)參數(shù)匹配到的結(jié)果。參考這個(gè)指定一個(gè)函數(shù)作為參數(shù)雀摘。
有了這句話裸删,其實(shí)就很好實(shí)現(xiàn)了,先看看具體代碼再做下一步分析阵赠。
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));
可以對(duì)照上面文檔的話來(lái)做分析:該函數(shù)的返回值(obj[key]=jawil
)將替換掉第一個(gè)參數(shù)(match=={{name}}
)匹配到的結(jié)果。
簡(jiǎn)單分析一下:.*?
是正則固定搭配用法清蚀,表示非貪婪匹配模式匕荸,盡可能匹配少的,什么意思呢枷邪?舉個(gè)簡(jiǎn)單的例子榛搔。
先看一個(gè)例子:
源字符串:aa<div>test1</div>bb<div>test2</div>cc
正則表達(dá)式一:<div>.*</div>
匹配結(jié)果一:<div>test1</div>bb<div>test2</div>
正則表達(dá)式二:<div>.*?</div>
匹配結(jié)果二:<div>test1</div>(這里指的是一次匹配結(jié)果,不使用/g东揣,所以沒(méi)包括<div>test2</div>)
根據(jù)上面的例子践惑,從匹配行為上分析一下,什是貪婪與非貪婪匹配模式嘶卧。
利用非貪婪匹配模就能匹配到所有的{{name}}
童本,{{age}}
,上面的也說(shuō)到過(guò)正則分組脸候,分組匹配到的就是 name
,也就是 function
的第二個(gè)參數(shù) key
绑蔫。
所以這行代碼的意思就很清楚运沦,正則匹配到{{name}}
,分組獲取 name
配深,然后把 {{name}}
替換成 obj[name](jawil)
携添。
當(dāng)然后來(lái)發(fā)現(xiàn)還有一個(gè)小問(wèn)題,如果有空格的話就會(huì)匹配失敗篓叶,類似這種寫法:
const template = "{{name }}很厲name害烈掠,才{{age }}歲";
所以在上面的基礎(chǔ)上還要去掉空格羞秤,其實(shí)也很簡(jiǎn)單,用正則或者 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));
將函數(shù)掛到 String 的原型鏈,得到最終版本
甚至矫限,我們可以通過(guò)修改原型鏈哺哼,實(shí)現(xiàn)一些很酷的效果:
String.prototype.render = function (context) {
return this.replace(/\{\{(.*?)\}\}/g, (match, key) => context[key.trim()]);
};
如果{}中間不是數(shù)字,則{}本身不需要轉(zhuǎn)義叼风,所以最終最簡(jiǎn)潔的代碼:
String.prototype.render = function (context) {
return this.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()]);
};
之后取董,我們便可以這樣調(diào)用啦:
"{{name}}很厲name害,才{{ age }}歲".render({ name: "jawil", age: "15" });
收獲
通過(guò)一個(gè)小小的模板字符串的實(shí)現(xiàn)无宿,領(lǐng)悟到要把一個(gè)功能實(shí)現(xiàn)不難茵汰,要把做到完美真是難上加難,需要對(duì)基礎(chǔ)掌握牢固孽鸡,有一定的沉淀蹂午,然后不斷地打磨才能比較優(yōu)雅的實(shí)現(xiàn),通過(guò)由一個(gè)很小的點(diǎn)往往可以拓展出很多的知識(shí)點(diǎn)梭灿。