一行代碼實(shí)現(xiàn)一個(gè)簡(jiǎn)單的模板字符串替換

起始

同許多初學(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 也給替換了拉一,從代碼我們可以看出二月的思路采盒。

  1. 代碼的作用目標(biāo)是 str,先用正則匹配出 {{name}}{{age}}蔚润,然后用分組獲取括號(hào)的 name,age,最后用 replace 方法把 {{name}}{{age}} 替換成 nameage磅氨,最后字符串就成了 name很name厲害,才age歲嫡纠,最后 for in 循環(huán)的時(shí)候才導(dǎo)致一起都被替換掉了烦租。
  2. for in 循環(huán)完全沒(méi)必要延赌,能不用 for in 盡量不要用 for infor 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)單明了了量没,就是把 objkey 值遍歷,然后拼成 {{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)之多也可以一定程度了解到 ReactVirtual DOM 的思想和優(yōu)越性。

除了用 for in 循環(huán)獲取 objkey 值趁冈,還可以用 Object.key() 獲取歼争,Object.getOwnPropertyNames() 以及 Reflect.ownKeys()也可以獲取,那么這幾種有啥區(qū)別呢渗勘?這里就簡(jiǎn)單說(shuō)一下他們的一些區(qū)別沐绒。

for...in循環(huán):會(huì)遍歷對(duì)象自身的屬性,以及原型屬性旺坠,for...in 循環(huán)只遍歷可枚舉(不包括 enumerablefalse )屬性乔遮。像 ArrayObject 使用內(nèi)置構(gòu)造函數(shù)所創(chuàng)建的對(duì)象都會(huì)繼承自 Object.prototypeString.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)梭灿。

一張圖快速入門正則表達(dá)式:

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末画侣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子堡妒,更是在濱河造成了極大的恐慌配乱,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皮迟,死亡現(xiàn)場(chǎng)離奇詭異搬泥,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)伏尼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門忿檩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人爆阶,你說(shuō)我怎么就攤上這事燥透。” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)钦铺。 經(jīng)常有香客問(wèn)我余爆,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮占遥,結(jié)果婚禮上理盆,老公的妹妹穿的比我還像新娘痘煤。我一直安慰自己,他們只是感情好猿规,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布衷快。 她就那樣靜靜地躺著,像睡著了一般坎拐。 火紅的嫁衣襯著肌膚如雪烦磁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天哼勇,我揣著相機(jī)與錄音都伪,去河邊找鬼。 笑死积担,一個(gè)胖子當(dāng)著我的面吹牛陨晶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播帝璧,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼先誉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了的烁?” 一聲冷哼從身側(cè)響起褐耳,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渴庆,沒(méi)想到半個(gè)月后铃芦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡襟雷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年刃滓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耸弄。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咧虎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出计呈,到底是詐尸還是另有隱情砰诵,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布捌显,位于F島的核電站茁彭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏苇瓣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一偿乖、第九天 我趴在偏房一處隱蔽的房頂上張望击罪。 院中可真熱鬧哲嘲,春花似錦、人聲如沸媳禁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)竣稽。三九已至囱怕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毫别,已是汗流浹背娃弓。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岛宦,地道東北人台丛。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像砾肺,于是被迫代替她去往敵國(guó)和親挽霉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容