本篇接着上篇 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,对做者也是一种鼓励。