摘要:前言本篇接著上篇系列之實(shí)現(xiàn)一個(gè)模板引擎上。字符串中的每個(gè)字符均可由一個(gè)轉(zhuǎn)義序列表示。在中,有四個(gè)字符被認(rèn)為是行終結(jié)符,其他的折行字符都會(huì)被視為空白。
前言
本篇接著上篇 underscore 系列之實(shí)現(xiàn)一個(gè)模板引擎(上)。
鑒于本篇涉及的知識(shí)點(diǎn)太多,我們先來(lái)介紹下會(huì)用到的知識(shí)點(diǎn)。
反斜杠的作用var txt = "We are the so-called "Vikings" from the north." console.log(txt);
我們的本意是想打印帶 "" 包裹的 Vikings 字符串,但是在 JavaScript 中,字符串使用單引號(hào)或者雙引號(hào)來(lái)表示起始或者結(jié)束,這段代碼會(huì)報(bào) Unexpected identifier 錯(cuò)誤。
如果我們就是想要在字符串中使用單引號(hào)或者雙引號(hào)呢?
我們可以使用反斜杠用來(lái)在文本字符串中插入省略號(hào)、換行符、引號(hào)和其他特殊字符:
var txt = "We are the so-called "Vikings" from the north." console.log(txt);
現(xiàn)在 JavaScript 就可以輸出正確的文本字符串了。
這種由反斜杠后接字母或數(shù)字組合構(gòu)成的字符組合就叫做“轉(zhuǎn)義序列”。
值得注意的是,轉(zhuǎn)義序列會(huì)被視為單個(gè)字符。
我們常見(jiàn)的轉(zhuǎn)義序列還有 表示換行、 表示制表符、 表示回車(chē)等等。
轉(zhuǎn)義序列在 JavaScript 中,字符串值是一個(gè)由零或多個(gè) Unicode 字符(字母、數(shù)字和其他字符)組成的序列。
字符串中的每個(gè)字符均可由一個(gè)轉(zhuǎn)義序列表示。比如字母 a,也可以用轉(zhuǎn)義序列 u0061 表示。
轉(zhuǎn)義序列以反斜杠 開(kāi)頭,它的作用是告知 JavaScript 解釋器下一個(gè)字符是特殊字符。轉(zhuǎn)義序列的語(yǔ)法為 uhhhh,其中 hhhh 是四位十六進(jìn)制數(shù)。
根據(jù)這個(gè)規(guī)則,我們可以算出常見(jiàn)字符的轉(zhuǎn)義序列,以字母 m 為例:
// 1. 求出字符 `m` 對(duì)應(yīng)的 unicode 值 var unicode = "m".charCodeAt(0) // 109 // 2. 轉(zhuǎn)成十六進(jìn)制 var result = unicode.toString(16); // "6d"
我們就可以使用 u006d 表示 m,不信你可以直接在瀏覽器命令行中直接輸入字符串 "u006d",看下打印結(jié)果。
值得注意的是: 雖然也是一種轉(zhuǎn)義序列,但是也可以使用上面的方式:
var unicode = " ".charCodeAt(0) // 10 var result = unicode.toString(16); // "a"
所以我們可以用 u000A 來(lái)表示換行符 ,比如在瀏覽器命令行中直接輸入 "a b" 和 "a u000A b" 效果是一樣的。
講了這么多,我們來(lái)看看一些常用字符的轉(zhuǎn)義序列以及含義:
Unicode 字符值 | 轉(zhuǎn)義序列 | 含義 |
u0009 | t | 制表符 |
u000A | n | 換行 |
u000D | r | 回車(chē) |
u0022 | " | 雙引號(hào) |
u0027 | " | 單引號(hào) |
u005C | 反斜杠 | |
u2028 | 行分隔符 | |
u2029 | 段落分隔符 |
Line Terminators,中文譯文行終結(jié)符。像空白字符一樣,行終結(jié)符可用于改善源文本的可讀性。
在 ES5 中,有四個(gè)字符被認(rèn)為是行終結(jié)符,其他的折行字符都會(huì)被視為空白。
這四個(gè)字符如下所示:
字符編碼值 | 名稱(chēng) |
---|---|
u000A | 換行符 |
u000D | 回車(chē)符 |
u2028 | 行分隔符 |
u2029 | 段落分隔符 |
試想我們寫(xiě)這樣一段代碼,能否正確運(yùn)行:
var log = new Function("var a = "1 23";console.log(a)"); log()
答案是可以,那下面這段呢:
var log = new Function("var a = "1 23";console.log(a)"); log()
答案是不可以,會(huì)報(bào)錯(cuò) Uncaught SyntaxError: Invalid or unexpected token。
這是為什么呢?
這是因?yàn)樵?Function 構(gòu)造函數(shù)的實(shí)現(xiàn)中,首先會(huì)將函數(shù)體代碼字符串進(jìn)行一次 ToString 操作,這時(shí)候字符串變成了:
var a = "1 23";console.log(a)
然后再檢測(cè)代碼字符串是否符合代碼規(guī)范,在 JavaScript 中,字符串表達(dá)式中是不允許換行的,這就導(dǎo)致了報(bào)錯(cuò)。
為了避免這個(gè)問(wèn)題,我們需要將代碼修改為:
var log = new Function("var a = "1 23";console.log(a)"); log()
其實(shí)不止 ,其他三種 行終結(jié)符,如果你在字符串表達(dá)式中直接使用,都會(huì)導(dǎo)致報(bào)錯(cuò)!
之所以講這個(gè)問(wèn)題,是因?yàn)樵谀0逡娴膶?shí)現(xiàn)中,就是使用了 Function 構(gòu)造函數(shù),如果我們?cè)谀0遄址惺褂昧?行終結(jié)符,便有可能會(huì)出現(xiàn)一樣的錯(cuò)誤,所以我們必須要對(duì)這四種 行終結(jié)符 進(jìn)行特殊的處理。
特殊字符除了這四種 行終結(jié)符 之外,我們還要對(duì)兩個(gè)字符進(jìn)行處理。
一個(gè)是 。
比如說(shuō)我們的模板內(nèi)容中使用了:
var log = new Function("var a = "123";console.log(a)"); log(); // 1
其實(shí)我們是想打印 "123",但是因?yàn)榘? 當(dāng)成了特殊字符的標(biāo)記進(jìn)行處理,所以最終打印了 1。
同樣的道理,如果我們?cè)谑褂媚0逡娴臅r(shí)候,使用了 字符串,也會(huì)導(dǎo)致錯(cuò)誤的處理。
第二個(gè)是 "。
如果我們?cè)谀0逡嬷惺褂昧?",因?yàn)槲覀儠?huì)拼接諸如 p.push(" ") 等字符串,因?yàn)?" 的原因,字符串會(huì)被錯(cuò)誤拼接,也會(huì)導(dǎo)致錯(cuò)誤。
所以總共我們需要對(duì)六種字符進(jìn)行特殊處理,處理的方式,就是正則匹配出這些特殊字符,然后比如將 替換成 , 替換成 ," 替換成 ",處理的代碼為:
var escapes = { """: """, "": "", " ": "r", " ": "n", "u2028": "u2028", "u2029": "u2029" }; var escapeRegExp = /|"| | |u2028|u2029/g; var escapeChar = function(match) { return "" + escapes[match]; };
我們測(cè)試一下:
var str = "console.log("I am Kevin");"; var newStr = str.replace(escapeRegExp, escapeChar); eval(newStr) // I am // Kevinreplace
我們來(lái)講一講字符串的 replace 函數(shù):
語(yǔ)法為:
str.replace(regexp|substr, newSubStr|function)
replace 的第一個(gè)參數(shù),可以傳一個(gè)字符串,也可以傳一個(gè)正則表達(dá)式。
第二個(gè)參數(shù),可以傳一個(gè)新字符串,也可以傳一個(gè)函數(shù)。
我們重點(diǎn)看下傳入函數(shù)的情況,簡(jiǎn)單舉一個(gè)例子:
var str = "hello world"; var newStr = str.replace("world", function(match){ return match + "!" }) console.log(newStr); // hello world!
match 表示匹配到的字符串,但函數(shù)的參數(shù)其實(shí)不止有 match,我們看個(gè)更復(fù)雜的例子:
function replacer(match, p1, p2, p3, offset, string) { // match,表示匹配的子串 abc12345#$*% // p1,第 1 個(gè)括號(hào)匹配的字符串 abc // p2,第 2 個(gè)括號(hào)匹配的字符串 12345 // p3,第 3 個(gè)括號(hào)匹配的字符串 #$*% // offset,匹配到的子字符串在原字符串中的偏移量 0 // string,被匹配的原字符串 abc12345#$*% return [p1, p2, p3].join(" - "); } var newString = "abc12345#$*%".replace(/([^d]*)(d*)([^w]*)/, replacer); // abc - 12345 - #$*%
另外要注意的是,如果第一個(gè)參數(shù)是正則表達(dá)式,并且其為全局匹配模式, 那么這個(gè)方法將被多次調(diào)用,每次匹配都會(huì)被調(diào)用。
舉個(gè)例子,如果我們要在一段字符串中匹配出 <%=xxx%> 中的值:
var str = "
傳入的函數(shù)會(huì)被執(zhí)行兩次,第一次的打印結(jié)果為:
<%=www.baidu.com%> www.baidu.com 13
第二次的打印結(jié)果為:
<%=baidu%> "baidu" 33
當(dāng)我們要建立一個(gè)正則表達(dá)式的時(shí)候,我們可以直接創(chuàng)建:
var reg = /ab+c/i;
也可以使用構(gòu)造函數(shù)的方式:
new RegExp("ab+c", "i");
值得一提的是:每個(gè)正則表達(dá)式對(duì)象都有一個(gè) source 屬性,返回當(dāng)前正則表達(dá)式對(duì)象的模式文本的字符串:
var regex = /fooBar/ig; console.log(regex.source); // "fooBar",不包含 /.../ 和 "ig"。正則表達(dá)式的特殊字符
正則表達(dá)式中有一些特殊字符,比如 d 就表示了匹配一個(gè)數(shù)字,等價(jià)于 [0-9]。
在上節(jié),我們使用 /<%=(.+?)%>/g 來(lái)匹配 <%=xxx%>,然而在 underscore 的實(shí)現(xiàn)中,用的卻是 /<%=([sS]+?)%>/g。
我們知道 s 表示匹配一個(gè)空白符,包括空格、制表符、換頁(yè)符、換行符和其他 Unicode 空格,S
匹配一個(gè)非空白符,[sS]就表示匹配所有的內(nèi)容,可是為什么我們不直接使用 . 呢?
我們可能以為 . 匹配任意單個(gè)字符,實(shí)際上,并不是如此, .匹配除行終結(jié)符之外的任何單個(gè)字符,不信我們做個(gè)試驗(yàn):
var str = "<%=hello world%>" str.replace(/<%=(.+?)%>/g, function(match){ console.log(match); // <%=hello world%> })
但是如果我們?cè)?hello world 之間加上一個(gè)行終結(jié)符,比如說(shuō) "u2029":
var str = "<%=hello u2029 world%>" str.replace(/<%=(.+?)%>/g, function(match){ console.log(match); })
因?yàn)槠ヅ洳坏剑砸膊粫?huì)執(zhí)行 console.log 函數(shù)。
但是改成 /<%=([sS]+?)%>/g 就可以正常匹配:
var str = "<%=hello u2029 world%>" str.replace(/<%=([sS]+?)%>/g, function(match){ console.log(match); // <%=hello ? world%> })惰性匹配
仔細(xì)看 /<%=([sS]+?)%>/g 這個(gè)正則表達(dá)式,我們知道 x+ 表示匹配 x 1 次或多次。x?表示匹配 x 0 次或 1 次,但是 +? 是個(gè)什么鬼?
實(shí)際上,如果在數(shù)量詞 *、+、? 或 {}, 任意一個(gè)后面緊跟該符號(hào)(?),會(huì)使數(shù)量詞變?yōu)榉秦澙罚?non-greedy) ,即匹配次數(shù)最小化。反之,默認(rèn)情況下,是貪婪的(greedy),即匹配次數(shù)最大化。
舉個(gè)例子:
console.log("aaabc".replace(/a+/g, "d")); // dbc console.log("aaabc".replace(/a+?/g, "d")); // ffffdbc
在這里我們應(yīng)該使用非惰性匹配,舉個(gè)例子:
var str = "
如果我們使用惰性匹配:
var str = "
講完需要的知識(shí)點(diǎn),我們開(kāi)始講 underscore 模板引擎的實(shí)現(xiàn)。
與我們上篇使用數(shù)組的 push ,最后再 join 的方法不同,underscore 使用的是字符串拼接的方式。
比如下面這樣一段模板字符串:
<%for ( var i = 0; i < users.length; i++ ) { %>
我們先將 <%=xxx%> 替換成 "+ xxx +",再將 <%xxx%> 替換成 "; xxx __p+=":
";for ( var i = 0; i < users.length; i++ ) { __p+="
這段代碼肯定會(huì)運(yùn)行錯(cuò)誤的,所以我們?cè)偬砑有╊^尾代碼,然后組成一個(gè)完整的代碼字符串:
var __p=""; with(obj){ __p+=" ";for ( var i = 0; i < users.length; i++ ) { __p+="
整理下代碼就是:
var __p=""; with(obj){ __p+=""; for ( var i = 0; i < users.length; i++ ) { __p+="
然后我們將 __p 這段代碼字符串傳入 Function 構(gòu)造函數(shù)中:
var render = new Function(data, __p)
我們執(zhí)行這個(gè) render 函數(shù),傳入需要的 data 數(shù)據(jù),就可以返回一段 HTML 字符串:
render(data)第五版 - 特殊字符的處理
我們接著上篇的第四版進(jìn)行書(shū)寫(xiě),不過(guò)加入對(duì)特殊字符的轉(zhuǎn)義以及使用字符串拼接的方式:
// 第五版 var settings = { // 求值 evaluate: /<%([sS]+?)%>/g, // 插入 interpolate: /<%=([sS]+?)%>/g, }; var escapes = { """: """, "": "", " ": "r", " ": "n", "u2028": "u2028", "u2029": "u2029" }; var escapeRegExp = /|"| | |u2028|u2029/g; var template = function(text) { var source = "var __p=""; "; source = source + "with(obj){ " source = source + "__p+=""; var main = text .replace(escapeRegExp, function(match) { return "" + escapes[match]; }) .replace(settings.interpolate, function(match, interpolate){ return ""+ " + interpolate + "+ "" }) .replace(settings.evaluate, function(match, evaluate){ return ""; " + evaluate + " __p+="" }) source = source + main + ""; }; return __p;"; console.log(source) var render = new Function("obj", source); return render; };
完整的使用代碼可以參考 template 示例五。
第六版 - 特殊值的處理不過(guò)有一點(diǎn)需要注意的是:
如果數(shù)據(jù)中 users[i].url 不存在怎么辦?此時(shí)取值的結(jié)果為 undefined,我們知道:
"1" + undefined // "1undefined"
就相當(dāng)于拼接了 undefined 字符串,這肯定不是我們想要的。我們可以在代碼中加入一點(diǎn)判斷:
.replace(settings.interpolate, function(match, interpolate){ return ""+ " + (interpolate == null ? "" : interpolate) + "+ "" })
但是吧,我就是不喜歡寫(xiě)兩遍 interpolate …… 嗯?那就這樣吧:
var source = "var __t, __p=""; "; ... .replace(settings.interpolate, function(match, interpolate){ return ""+ ((__t=(" + interpolate + "))==null?"":__t)+ "" })
其實(shí)就相當(dāng)于:
var __t; var result = (__t = interpolate) == null ? "" : __t;
完整的使用代碼可以參考 template 示例六。
第七版現(xiàn)在我們使用的方式是將模板字符串進(jìn)行多次替換,然而在 underscore 的實(shí)現(xiàn)中,只進(jìn)行了一次替換,我們來(lái)看看 underscore 是怎么實(shí)現(xiàn)的:
var template = function(text) { var matcher = RegExp([ (settings.interpolate).source, (settings.evaluate).source ].join("|") + "|$", "g"); var index = 0; var source = "__p+=""; text.replace(matcher, function(match, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escapeRegExp, function(match) { return "" + escapes[match]; }); index = offset + match.length; if (interpolate) { source += ""+ ((__t=(" + interpolate + "))==null?"":__t)+ ""; } else if (evaluate) { source += ""; " + evaluate + " __p+=""; } return match; }); source += ""; "; source = "with(obj||{}){ " + source + "} " source = "var __t, __p="";" + source + "return __p; "; var render = new Function("obj", source); return render; };
其實(shí)原理也很簡(jiǎn)單,就是在執(zhí)行多次匹配函數(shù)的時(shí)候,不斷復(fù)制字符串,處理字符串,拼接字符串,最后拼接首尾代碼,得到最終的代碼字符串。
不過(guò)值得一提的是:在這段代碼里,matcher 的表達(dá)式最后為:/<%=([sS]+?)%>|<%([sS]+?)%>|$/g
問(wèn)題是為什么還要加個(gè) |$ 呢?我們來(lái)看下 $:
var str = "abc"; str.replace(/$/g, function(match, offset){ console.log(typeof match) // 空字符串 console.log(offset) // 3 return match })
我們之所以匹配 $,是為了獲取最后一個(gè)字符串的位置,這樣當(dāng)我們 text.slice(index, offset)的時(shí)候,就可以截取到最后一個(gè)字符。
完整的使用代碼可以參考 template 示例七。
最終版其實(shí)代碼寫(xiě)到這里,就已經(jīng)跟 underscore 的實(shí)現(xiàn)很接近了,只是 underscore 加入了更多細(xì)節(jié)的處理,比如:
對(duì)數(shù)據(jù)的轉(zhuǎn)義功能
可傳入配置項(xiàng)
對(duì)錯(cuò)誤的處理
添加 source 屬性,以方便查看代碼字符串
添加了方便調(diào)試的 print 函數(shù)
...
但是這些內(nèi)容都還算簡(jiǎn)單,就不一版一版寫(xiě)了,最后的版本在 template 示例八,如果對(duì)其中有疑問(wèn),歡迎留言討論。
underscore 系列underscore 系列目錄地址:https://github.com/mqyqingfeng/Blog。
underscore 系列預(yù)計(jì)寫(xiě)八篇左右,重點(diǎn)介紹 underscore 中的代碼架構(gòu)、鏈?zhǔn)秸{(diào)用、內(nèi)部函數(shù)、模板引擎等內(nèi)容,旨在幫助大家閱讀源碼,以及寫(xiě)出自己的 undercore。
如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)務(wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對(duì)作者也是一種鼓勵(lì)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/92760.html
摘要:所以它與其他系列的文章并不沖突,完全可以在閱讀完這個(gè)系列后,再跟著其他系列的文章接著學(xué)習(xí)。如何閱讀我在寫(xiě)系列的時(shí)候,被問(wèn)的最多的問(wèn)題就是該怎么閱讀源碼我想簡(jiǎn)單聊一下自己的思路。感謝大家的閱讀和支持,我是冴羽,下個(gè)系列再見(jiàn)啦 前言 別名:《underscore 系列 8 篇正式完結(jié)!》 介紹 underscore 系列是我寫(xiě)的第三個(gè)系列,前兩個(gè)系列分別是 JavaScript 深入系列、...
摘要:第一版我們來(lái)嘗試實(shí)現(xiàn)第一版第一版為了驗(yàn)證是否有用文件文件完整的可以查看示例一在這里我們使用了,實(shí)際上在文章中使用的是構(gòu)造函數(shù)。構(gòu)造函數(shù)創(chuàng)建一個(gè)新的對(duì)象。 前言 underscore 提供了模板引擎的功能,舉個(gè)例子: var tpl = hello: ; var compiled = _.template(tpl); compiled({name: Kevin}); // hello:...
摘要:前端模板的出現(xiàn)使得前后端分離成為可能。總結(jié)本文簡(jiǎn)單介紹了模板引擎在前后端的使用,下文我們回到,重點(diǎn)分析下的使用方式以及源碼原理。樓主對(duì)于模板引擎的認(rèn)識(shí)比較淺顯,有不正之處希望指出感謝 前言 這篇文章本來(lái)不打算寫(xiě)的,實(shí)話說(shuō)樓主對(duì)前端模板的認(rèn)識(shí)還處在非常初級(jí)的階段,但是為了整個(gè) 源碼解讀系列 的完整性,在深入 Underscore _.template 方法源碼后,覺(jué)得還是有必要記下此文,...
摘要:你可以輕松為你的函數(shù)庫(kù)添加防沖突功能。系列系列目錄地址。如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)務(wù)必給予指正,十分感謝。 防沖突 underscore 使用 _ 作為函數(shù)的掛載對(duì)象,如果頁(yè)面中已經(jīng)存在了 _ 對(duì)象,underscore 就會(huì)覆蓋該對(duì)象,舉個(gè)例子: var _ = {value: 1 } // 引入 underscore 后 console.log(_.value); // un...
摘要:與最后,使用我們的寫(xiě)的函數(shù)重寫(xiě)下函數(shù)系列系列目錄地址。系列預(yù)計(jì)寫(xiě)八篇左右,重點(diǎn)介紹中的代碼架構(gòu)鏈?zhǔn)秸{(diào)用內(nèi)部函數(shù)模板引擎等內(nèi)容,旨在幫助大家閱讀源碼,以及寫(xiě)出自己的。如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)務(wù)必給予指正,十分感謝。 partial 在《 JavaScript 專(zhuān)題之偏函數(shù)》中,我們寫(xiě)了一個(gè) partial 函數(shù),用來(lái)固定函數(shù)的部分參數(shù),實(shí)現(xiàn)代碼如下: // 這是文章中的第一版 fu...
閱讀 3793·2023-04-25 16:32
閱讀 2194·2021-09-28 09:36
閱讀 2034·2021-09-06 15:02
閱讀 673·2021-09-02 15:21
閱讀 918·2019-08-30 15:56
閱讀 3512·2019-08-30 15:45
閱讀 1708·2019-08-30 13:09
閱讀 379·2019-08-29 16:05