媒介
本篇接着上篇 underscore 系列之完成一个模板引擎(上)。
鉴于本篇触及的知识点太多,我们先来引见下会用到的知识点。
反斜杠的作用
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 就能够输出准确的文本字符串了。
这类由反斜杠后接字母或数字组合构成的字符组合就叫做“转义序列”。
值得注重的是,转义序列会被视为单个字符。
我们罕见的转义序列另有 \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
Line Terminators,中文译文行终结符
。像空缺字符一样,行终结符
可用于改良源文本的可读性。
在 ES5 中,有四个字符被认为是行终结符
,其他的折行字符都邑被视为空缺。
这四个字符以下所示:
字符编码值 | 称号 |
---|---|
u000A | 换行符 |
u000D | 回车符 |
u2028 | 行分隔符 |
u2029 | 段落分隔符 |
Function
试想我们写如许一段代码,可否准确运转:
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
实在我们是想打印 ‘123’,然则由于把 \
当成了特别字符的标记举行处置惩罚,所以终究打印了 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
我们来说一讲字符串的 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
婚配一个非空缺符,[sS]就示意婚配一切的内容,但是为何我们不直接运用 .
呢?
我们能够认为 .
婚配恣意单个字符,实际上,并非云云, .
婚配除行终结符
以外的任何单个字符,不信我们做个实验:
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%>
template
讲完须要的知识点,我们最先讲 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 到场了更多细节的处置惩罚,比方:
- 对数据的转义功用
- 可传入设置项
- 对毛病的处置惩罚
- 增加 source 属性,以轻易检察代码字符串
- 增加了轻易调试的 print 函数
- …
然则这些内容都还算简朴,就不一版一版写了,末了的版本在 template 示例八,假如对个中有疑问,迎接留言议论。
underscore 系列
underscore 系列目次地点:https://github.com/mqyqingfeng/Blog。
underscore 系列估计写八篇摆布,重点引见 underscore 中的代码架构、链式挪用、内部函数、模板引擎等内容,旨在协助人人浏览源码,以及写出本身的 undercore。
假如有毛病或许不严谨的处所,请务必赋予斧正,非常谢谢。假如喜好或许有所启示,迎接 star,对作者也是一种勉励。