1. 程式人生 > >一行程式碼實現一個簡單的模板字串替換

一行程式碼實現一個簡單的模板字串替換

起始

同許多初學 Javascript 的菜鳥一樣,起初,我也是採用拼接字串的形式,將 JSON 資料嵌入 HTML 中。開始時程式碼量較少,暫時還可以接受。但當頁面結構複雜起來後,其弱點開始變得無法忍受起來:

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

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

於是出來了後來的 ES6

ES6的模板字串用起來著實方便,對於比較老的專案,專案沒webpackgulp 等構建工具,無法使用 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 也給替換了,從程式碼我們可以看出二月的思路。

  1. 程式碼的作用目標是 str,先用正則匹配出 {{name}}{{age}},然後用分組獲取括號的 name,age,最後用 replace 方法把 {{name}}{{age}} 替換成 nameage,最後字串就成了 name很name厲害,才age歲,最後 for in 迴圈的時候才導致一起都被替換掉了。
  2. for in 迴圈完全沒必要,能不用 for in 儘量不要用 for infor in 會遍歷自身以及原型鏈所有的屬性。

志欽童鞋:

JavaScript
123456789101112131415 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 迴圈獲取 objkey 值,還可以用 Object.key() 獲取,Object.getOwnPropertyNames() 以及 Reflect.ownKeys()也可以獲取,那麼這幾種有啥區別呢?這裡就簡單說一下他們的一些區別。

for...in迴圈:會遍歷物件自身的屬性,以及原型屬性,for...in 迴圈只遍歷可列舉(不包括 enumerablefalse )屬性。像 ArrayObject 使用內建建構函式所建立的物件都會繼承自 Object.prototypeString.prototype 的不可列舉屬性;Object.key():可以得到自身可列舉的屬性,但得不到原型鏈上的屬性;

Object.getOwnPropertyNames():可以得到自身所有的屬性(包括不可列舉),但得不到原型鏈上的屬性, Symbols 屬性也得不到.

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

JavaScript
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}})匹配到的結果。
簡單分析一下:.*? 是正則固定搭配用法,表示非貪婪匹配模式,儘可能匹配少的,什麼意思呢?舉個簡單的例子。

先看一個例子:

JavaScript
123456789 源字串:aa<div>test1</div>bb<div>test2</div>cc正則表示式一:<div>.*</div>匹配結果一:<div>test1<