一行程式碼實現一個簡單的模板字串替換
起始
同許多初學 Javascript
的菜鳥一樣,起初,我也是採用拼接字串的形式,將 JSON
資料嵌入 HTML
中。開始時程式碼量較少,暫時還可以接受。但當頁面結構複雜起來後,其弱點開始變得無法忍受起來:
- 書寫不連貫。每寫一個變數就要斷一下,插入一個 + 和 “。十分容易出錯。
- 無法重用。
HTML
片段都是離散化的資料,難以對其中重複的部分進行提取。 - 無法很好地利用<tempalte> 標籤。這是
HTML5
中新增的一個標籤,標準極力推薦將HTML
模板放入<template> 標籤中,使程式碼更簡潔。
當時我的心情就是這樣的:
這TMD是在逗我嗎。
於是出來了後來的 ES6
webpack
,gulp
等構建工具,無法使用 ES6
的語法,但是想也借鑑這種優秀的處理字串拼接的方式,我們不妨可以試著自己寫一個,主要是思路,可以使用 ES6
語法模擬 ES6的模板字串的這個功能。
後端返回的一般都是 JSON
的資料格式,所以我們按照下面的規則進行模擬。
需求描述
實現一個 render(template, context) 方法,將 template 中的佔位符用 context 填充。
要求:
不需要有控制流成分(如 迴圈、條件 等等),只要有變數替換功能即可
級聯的變數也可以展開
被轉義的的分隔符 { 和 } 不應該被渲染,分隔符與變數之間允許有空白字元
123 | varobj={name:"二月",age:"15"};varstr="{{name}}很厲害,才{{age}}歲";輸出:二月很厲害,才15歲。<strong>PS:本文需要對<ahref="https://github.com/jawil/blog/issues/20">正則表示式</a>有一定的瞭解,如果還不瞭解<ahref="https://github.com/jawil/blog/issues/20">正則表示式</a>,建議先去學習一下,正則也是面試筆試必備的技能,上面連結末尾有不少正則學習的連結。</strong> |
如果是你,你會怎麼實現?可以先嚐試自己寫寫,實現也不難。
先不說我的實現,我把這個題給其他好友做的時候,實現的不盡相同,我們先看幾位童鞋的實現,然後在他們的基礎上找到常見的誤區以及實現不夠優雅的地方。
二月童鞋:
1234567891011 | let str="{{name}}很厲害,才{{age}}歲"let obj={name:'二月',age:15}functiontest(str,obj){let _s=str.replace(/\{\{(\w+)\}\}/g,'$1')let resultfor(letkinobj){_s=_s.replace(newRegExp(k,'g'),obj[k])}return_s}consts=test(str,obj) |
最基本的是實現了,但是程式碼還是有很多問題沒考慮到,首先 Object 的 key 值不一定只是 w,
還有就是如果字串是這種的:
12 | 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
會遍歷自身以及原型鏈所有的屬性。
志欽童鞋:
JavaScript123456789101112131415 | varstr="{{name}}很厲害,才{{age}}歲";varstr2="{{name}}很厲name害,才{{age}}歲{{name}}";varobj={name:'周杰倫',age:15};functionfun(str,obj){vararr;arr=str.match(/{{[a-zA-Z\d]+}}/g);for(vari=0;i<arr.length;i++){arr[i]=arr[i].replace(/{{|}}/g,'');str=str.replace('{{'+arr[i]+'}}',obj[arr[i]]);}returnstr;}console.log(fun(str,obj));console.log(fun(str2,obj)); |
思路是正確的,知道最後要替換的是 {{name}}
和 {{age}}
整體,而不是像二月童鞋那樣最後去替換 name
,所有跑起來肯定沒問題,實現是實現了但是感覺有點那個,我們要探討的是一行程式碼也就是程式碼越少越好。
小維童鞋:
1234567891011 | functiona(str,obj){varstr1=str;for(varkey inobj){varre=newRegExp("{{"+key+"}}","g");str1=str1.replace(re,obj[key]);}console.log(str1);}conststr="{{name}}很厲name害{{name}},才{{age}}歲";constobj={name:"jawil",age:"15"};a(str,obj);實現的已經簡單明瞭了,就是把<code>obj</code>的<code>key</code>值遍歷,然後拼成<code>{{key}}</code>,最後用<code>obj[key]</code>也就是<code>value</code>把<code>{{key}}</code>整個給替換了,思路很好,跟我最初的版本一個樣。 |
我的實現:
123456789 | functionparseString(str,obj){Object.keys(obj).forEach(key=>{str=str.replace(newRegExp(`{{${key}}}`,'g'),obj[key]);});returnstr;}conststr="{{name}}很厲name害{{name}},才{{age}}歲";constobj={name:"jawil",age:"15"};console.log(parseString(str,obj)); |
其實這裡還是有些問題的,首先我沒用 for…in 迴圈就是為了考慮不必要的迴圈,因為 for…in 迴圈會遍歷原型鏈所有的可列舉屬性,造成不必要的迴圈。
我們可以簡單看一個例子,看看 for…in的可怕性。
123456789 | // Chrome v63constdiv=document.createElement('div');letm=0;for(letkindiv){m++;}letn=0;console.log(m);// 231console.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 屬性也得不到.
上面說的可能比較抽象,不夠直觀。可以看個我寫的 DEMO
,一切簡單明鳥。
12345678910111213141516171819 | constparent={a:1,b:2,c:3};constchild={d:4,e:5,[Symbol()]:6};child.__proto__=parent;Object.defineProperty(child,"d",{enumerable:false});for(varattr inchild){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)
一個用來建立新子字串的函式,該函式的返回值將替換掉第一個引數匹配到的結果。參考這個指定一個函式作為引數。
有了這句話,其實就很好實現了,先看看具體程式碼再做下一步分析。
123456 | functionrender(template,context){returntemplate.replace(/\{\{(.*?)\}\}/g,(match,key)=>context[key]);}consttemplate="{{name}}很厲name害,才{{age}}歲";constcontext={name:"jawil",age:"15"};console.log(render(template,context)); |
可以對照上面文件的話來做分析:該函式的返回值(obj[key]=jawil
)將替換掉第一個引數(match=={{name}}
)匹配到的結果。
簡單分析一下:.*?
是正則固定搭配用法,表示非貪婪匹配模式,儘可能匹配少的,什麼意思呢?舉個簡單的例子。
先看一個例子:
JavaScript123456789 | 源字串:aa<div>test1</div>bb<div>test2</div>cc正則表示式一:<div>.*</div>匹配結果一:<div>test1< |