本篇接着上篇 underscore 系列之實現一個模板引擎(上)。git
鑑於本篇涉及的知識點太多,咱們先來介紹下會用到的知識點。github
var txt = "We are the so-called "Vikings" from the north."
console.log(txt);
複製代碼
咱們的本意是想打印帶 ""
包裹的 Vikings
字符串,可是在 JavaScript 中,字符串使用單引號或者雙引號來表示起始或者結束,這段代碼會報 Unexpected identifier
錯誤。正則表達式
若是咱們就是想要在字符串中使用單引號或者雙引號呢?數組
咱們可使用反斜槓用來在文本字符串中插入省略號、換行符、引號和其餘特殊字符:瀏覽器
var txt = "We are the so-called \"Vikings\" from the north."
console.log(txt);
複製代碼
如今 JavaScript 就能夠輸出正確的文本字符串了。bash
這種由反斜槓後接字母或數字組合構成的字符組合就叫作「轉義序列」。架構
值得注意的是,轉義序列會被視爲單個字符。ide
咱們常見的轉義序列還有 \n
表示換行、\t
表示製表符、\r
表示回車等等。函數
在 JavaScript 中,字符串值是一個由零或多個 Unicode 字符(字母、數字和其餘字符)組成的序列。測試
字符串中的每一個字符都可由一個轉義序列表示。好比字母 a
,也能夠用轉義序列 \u0061
表示。
轉義序列以反斜槓
\
開頭,它的做用是告知 JavaScript 解釋器下一個字符是特殊字符。
轉義序列的語法爲
\uhhhh
,其中 hhhh 是四位十六進制數。
根據這個規則,咱們能夠算出常見字符的轉義序列,以字母 m
爲例:
// 1. 求出字符 `m` 對應的 unicode 值
var unicode = 'm'.charCodeAt(0) // 109
// 2. 轉成十六進制
var result = unicode.toString(16); // "6d"
複製代碼
咱們就可使用 \u006d
表示 m
,不信你能夠直接在瀏覽器命令行中直接輸入字符串 '\u006d'
,看下打印結果。
值得注意的是: \n
雖然也是一種轉義序列,可是也可使用上面的方式:
var unicode = '\n'.charCodeAt(0) // 10
var result = unicode.toString(16); // "a"
複製代碼
因此咱們能夠用 \u000A
來表示換行符 \n
,好比在瀏覽器命令行中直接輸入 'a \n b'
和 'a \u000A b'
效果是同樣的。
講了這麼多,咱們來看看一些經常使用字符的轉義序列以及含義:
Unicode 字符值 | 轉義序列 | 含義 |
\u0009 | \t | 製表符 |
\u000A | \n | 換行 |
\u000D | \r | 回車 |
\u0022 | \" | 雙引號 |
\u0027 | \' | 單引號 |
\u005C | \\ | 反斜槓 |
\u2028 | 行分隔符 | |
\u2029 | 段落分隔符 |
Line Terminators,中文譯文行終結符
。像空白字符同樣,行終結符
可用於改善源文本的可讀性。
在 ES5 中,有四個字符被認爲是行終結符
,其餘的折行字符都會被視爲空白。
這四個字符以下所示:
字符編碼值 | 名稱 |
---|---|
\u000A | 換行符 |
\u000D | 回車符 |
\u2028 | 行分隔符 |
\u2029 | 段落分隔符 |
試想咱們寫這樣一段代碼,可否正確運行:
var log = new Function("var a = '1\t23';console.log(a)");
log()
複製代碼
答案是能夠,那下面這段呢:
var log = new Function("var a = '1\n23';console.log(a)");
log()
複製代碼
答案是不能夠,會報錯 Uncaught SyntaxError: Invalid or unexpected token
。
這是爲何呢?
這是由於在 Function 構造函數的實現中,首先會將函數體代碼字符串進行一次 ToString
操做,這時候字符串變成了:
var a = '1 23';console.log(a)
複製代碼
而後再檢測代碼字符串是否符合代碼規範,在 JavaScript 中,字符串表達式中是不容許換行的,這就致使了報錯。
爲了不這個問題,咱們須要將代碼修改成:
var log = new Function("var a = '1\\n23';console.log(a)");
log()
複製代碼
其實不止 \n
,其餘三種 行終結符
,若是你在字符串表達式中直接使用,都會致使報錯!
之因此講這個問題,是由於在模板引擎的實現中,就是使用了 Function 構造函數,若是咱們在模板字符串中使用了 行終結符
,便有可能會出現同樣的錯誤,因此咱們必需要對這四種 行終結符
進行特殊的處理。
除了這四種 行終結符
以外,咱們還要對兩個字符進行處理。
一個是 \
。
好比說咱們的模板內容中使用了\
:
var log = new Function("var a = '1\23';console.log(a)");
log(); // 1
複製代碼
其實咱們是想打印 '1\23',可是由於把 \
當成了特殊字符的標記進行處理,因此最終打印了 1。
一樣的道理,若是咱們在使用模板引擎的時候,使用了 \
字符串,也會致使錯誤的處理。
第二個是 '
。
若是咱們在模板引擎中使用了 '
,由於咱們會拼接諸如 p.push('
')
等字符串,由於 '
的緣由,字符串會被錯誤拼接,也會致使錯誤。
因此總共咱們須要對六種字符進行特殊處理,處理的方式,就是正則匹配出這些特殊字符,而後好比將 \n
替換成 \\n
,\
替換成 \\
,'
替換成 \\'
,處理的代碼爲:
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
複製代碼
咱們測試一下:
var str = 'console.log("I am \n Kevin");';
var newStr = str.replace(escapeRegExp, escapeChar);
eval(newStr)
// I am
// Kevin
複製代碼
咱們來說一講字符串的 replace 函數:
語法爲:
str.replace(regexp|substr, newSubStr|function)
複製代碼
replace 的第一個參數,能夠傳一個字符串,也能夠傳一個正則表達式。
第二個參數,能夠傳一個新字符串,也能夠傳一個函數。
咱們重點看下傳入函數的狀況,簡單舉一個例子:
var str = 'hello world';
var newStr = str.replace('world', function(match){
return match + '!'
})
console.log(newStr); // hello world!
複製代碼
match 表示匹配到的字符串,但函數的參數其實不止有 match,咱們看個更復雜的例子:
function replacer(match, p1, p2, p3, offset, string) {
// match,表示匹配的子串 abc12345#$*%
// p1,第 1 個括號匹配的字符串 abc
// p2,第 2 個括號匹配的字符串 12345
// p3,第 3 個括號匹配的字符串 #$*%
// offset,匹配到的子字符串在原字符串中的偏移量 0
// string,被匹配的原字符串 abc12345#$*%
return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer); // abc - 12345 - #$*%
複製代碼
另外要注意的是,若是第一個參數是正則表達式,而且其爲全局匹配模式, 那麼這個方法將被屢次調用,每次匹配都會被調用。
舉個例子,若是咱們要在一段字符串中匹配出 <%=xxx%>
中的值:
var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>'
str.replace(/<%=(.+?)%>/g, function(match, p1, offset, string){
console.log(match);
console.log(p1);
console.log(offset);
console.log(string);
})
複製代碼
傳入的函數會被執行兩次,第一次的打印結果爲:
<%=www.baidu.com%>
www.baidu.com
13
<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>
複製代碼
第二次的打印結果爲:
<%=baidu%>
'baidu'
33
<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>
複製代碼
當咱們要創建一個正則表達式的時候,咱們能夠直接建立:
var reg = /ab+c/i;
複製代碼
也可使用構造函數的方式:
new RegExp('ab+c', 'i');
複製代碼
值得一提的是:每一個正則表達式對象都有一個 source 屬性,返回當前正則表達式對象的模式文本的字符串:
var regex = /fooBar/ig;
console.log(regex.source); // "fooBar",不包含 /.../ 和 "ig"。
複製代碼
正則表達式中有一些特殊字符,好比 \d
就表示了匹配一個數字,等價於 [0-9]。
在上節,咱們使用 /<%=(.+?)%>/g
來匹配 <%=xxx%>
,然而在 underscore 的實現中,用的倒是 /<%=([\s\S]+?)%>/g
。
咱們知道 \s 表示匹配一個空白符,包括空格、製表符、換頁符、換行符和其餘 Unicode 空格,\S 匹配一個非空白符,[\s\S]就表示匹配全部的內容,但是爲何咱們不直接使用 .
呢?
咱們可能覺得 .
匹配任意單個字符,實際上,並非如此, .
匹配除行終結符
以外的任何單個字符,不信咱們作個試驗:
var str = '<%=hello world%>'
str.replace(/<%=(.+?)%>/g, function(match){
console.log(match); // <%=hello world%>
})
複製代碼
可是若是咱們在 hello world 之間加上一個行終結符
,好比說 '\u2029':
var str = '<%=hello \u2029 world%>'
str.replace(/<%=(.+?)%>/g, function(match){
console.log(match);
})
複製代碼
由於匹配不到,因此也不會執行 console.log 函數。
可是改爲 /<%=([\s\S]+?)%>/g
就能夠正常匹配:
var str = '<%=hello \u2029 world%>'
str.replace(/<%=([\s\S]+?)%>/g, function(match){
console.log(match); // <%=hello
world%>
})
複製代碼
仔細看 /<%=([\s\S]+?)%>/g
這個正則表達式,咱們知道 x+
表示匹配 x
1 次或屢次。x?
表示匹配 x
0 次或 1 次,可是 +?
是個什麼鬼?
實際上,若是在數量詞 *、+、? 或 {}, 任意一個後面緊跟該符號(?),會使數量詞變爲非貪婪( non-greedy) ,即匹配次數最小化。反之,默認狀況下,是貪婪的(greedy),即匹配次數最大化。
舉個例子:
console.log("aaabc".replace(/a+/g, "d")); // dbc
console.log("aaabc".replace(/a+?/g, "d")); // dddbc
複製代碼
在這裏咱們應該使用非惰性匹配,舉個例子:
var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>'
str.replace(/<%=(.+?)%>/g, function(match){
console.log(match);
})
// <%=www.baidu.com%>
// <%=baidu%>
複製代碼
若是咱們使用惰性匹配:
var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>'
str.replace(/<%=(.+)%>/g, function(match){
console.log(match);
})
// <%=www.baidu.com%>"><%=baidu%>
複製代碼
講完須要的知識點,咱們開始講 underscore 模板引擎的實現。
與咱們上篇使用數組的 push ,最後再 join 的方法不一樣,underscore 使用的是字符串拼接的方式。
好比下面這樣一段模板字符串:
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
複製代碼
咱們先將 <%=xxx%>
替換成 '+ xxx +'
,再將 <%xxx%>
替換成 '; xxx __p+='
:
';for ( var i = 0; i < users.length; i++ ) { __p+='
<li>
<a href="'+ users[i].url + '">
'+ users[i].name +'
</a>
</li>
'; } __p+='
複製代碼
這段代碼確定會運行錯誤的,因此咱們再添加些頭尾代碼,而後組成一個完整的代碼字符串:
var __p='';
with(obj){
__p+=' ';for ( var i = 0; i < users.length; i++ ) { __p+=' <li> <a href="'+ users[i].url + '"> '+ users[i].name +' </a> </li> '; } __p+=' ';
};
return __p;
複製代碼
整理下代碼就是:
var __p='';
with(obj){
__p+='';
for ( var i = 0; i < users.length; i++ ) {
__p+='<li><a href="'+ users[i].url + '"> '+ users[i].name +'</a></li>';
}
__p+='';
};
return __p
複製代碼
而後咱們將 __p
這段代碼字符串傳入 Function 構造函數中:
var render = new Function(data, __p)
複製代碼
咱們執行這個 render 函數,傳入須要的 data 數據,就能夠返回一段 HTML 字符串:
render(data)
複製代碼
咱們接着上篇的第四版進行書寫,不過加入對特殊字符的轉義以及使用字符串拼接的方式:
// 第五版
var settings = {
// 求值
evaluate: /<%([\s\S]+?)%>/g,
// 插入
interpolate: /<%=([\s\S]+?)%>/g,
};
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var template = function(text) {
var source = "var __p='';\n";
source = source + "with(obj){\n"
source = source + "__p+='";
var main = text
.replace(escapeRegExp, function(match) {
return '\\' + escapes[match];
})
.replace(settings.interpolate, function(match, interpolate){
return "'+\n" + interpolate + "+\n'"
})
.replace(settings.evaluate, function(match, evaluate){
return "';\n " + evaluate + "\n__p+='"
})
source = source + main + "';\n }; \n return __p;";
console.log(source)
var render = new Function('obj', source);
return render;
};
複製代碼
完整的使用代碼能夠參考 template 示例五。
不過有一點須要注意的是:
若是數據中 users[i].url
不存在怎麼辦?此時取值的結果爲 undefined,咱們知道:
'1' + undefined // "1undefined"
複製代碼
就至關於拼接了 undefined 字符串,這確定不是咱們想要的。咱們能夠在代碼中加入一點判斷:
.replace(settings.interpolate, function(match, interpolate){
return "'+\n" + (interpolate == null ? '' : interpolate) + "+\n'"
})
複製代碼
可是吧,我就是不喜歡寫兩遍 interpolate …… 嗯?那就這樣吧:
var source = "var __t, __p='';\n";
...
.replace(settings.interpolate, function(match, interpolate){
return "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"
})
複製代碼
其實就至關於:
var __t;
var result = (__t = interpolate) == null ? '' : __t;
複製代碼
完整的使用代碼能夠參考 template 示例六。
如今咱們使用的方式是將模板字符串進行屢次替換,然而在 underscore 的實現中,只進行了一次替換,咱們來看看 underscore 是怎麼實現的:
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 += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
return match;
});
source += "';\n";
source = 'with(obj||{}){\n' + source + '}\n'
source = "var __t, __p='';" +
source + 'return __p;\n';
var render = new Function('obj', source);
return render;
};
複製代碼
其實原理也很簡單,就是在執行屢次匹配函數的時候,不斷複製字符串,處理字符串,拼接字符串,最後拼接首尾代碼,獲得最終的代碼字符串。
不過值得一提的是:在這段代碼裏,matcher 的表達式最後爲:/<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
問題是爲何還要加個 |$
呢?咱們來看下 $:
var str = "abc";
str.replace(/$/g, function(match, offset){
console.log(typeof match) // 空字符串
console.log(offset) // 3
return match
})
複製代碼
咱們之因此匹配 $,是爲了獲取最後一個字符串的位置,這樣當咱們 text.slice(index, offset)的時候,就能夠截取到最後一個字符。
完整的使用代碼能夠參考 template 示例七。
其實代碼寫到這裏,就已經跟 underscore 的實現很接近了,只是 underscore 加入了更多細節的處理,好比:
可是這些內容都還算簡單,就不一版一版寫了,最後的版本在 template 示例八,若是對其中有疑問,歡迎留言討論。
underscore 系列目錄地址:github.com/mqyqingfeng…。
underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助你們閱讀源碼,以及寫出本身的 undercore。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。