转义序列
在javaScript中,字符串值是一个由零或多个 Unicode字符(字母、数字和其他字符)组成的序列。
字符串的每个字符均可由一个转义序列表示。比如字母 a , 也可以用转义序列 \u0061
表示。
转义序列以反斜杠
\
开头,它的作用是告知 javaScript解释器下一个字符是特殊字符
转义序列的语法为
\uhhhh
,其中 hhhh 是四位十六进制数。
根据这个规则,我们可以算出常见字符的转义序列,以字母 m 为例:
1 | // 1. 求出字符 m 对应的 unicode值 |
这里是一些常用字符的转义序列以及含义:
Unicode 字符值 | 转义序列 | 含义 |
---|---|---|
\u0009 | \t | 制表符 |
\u000A | \n | 换行 |
\u000D | \r | 回车 |
\u0022 | " | 双引号 |
\u0027 | ' | 单引号 |
\u005C | \ | 反斜杠 |
\u2028 | 行分隔符 | |
\u2029 | 段落分隔符 |
Line Terminators
Line Terminators, 中文译文 行终结符
。像空白符一样, 行终结符
可用于改善源文本的可读性。
在es5中,由四个字符被认为是 行终结符
,其他的折行字符都会被视为空白。
|字符编码值|名称|
|:——:|:——:|
| \u000A | 换行符 |
| \u000D | 回车符 |
| \u2028 | 行分隔符 |
| \u2029 | 段落分隔符 |
Function
试想我们写这样一段代码,能否正确运行:
1 | var log = new Function("var a = '1\t23';console.log(a)"); |
答案是可以,那下面这段呢:
1 | var log = new Function("var a = '1\n23';console.log(a)"); |
答案是不可以,会报错 VM42263:3 Uncaught SyntaxError: Invalid or unexpected token
这是为什么呢?
这是因为Function 构造函数的实现中,首先会将函数体代码字符串进行一次 ToString 操作,这时候字符串变成了:
1 | var a = '1 |
然后再检测代码字符串是否符合代码规范,在javaScript中,字符串表达式中是不允许换行的,这就导致了报错。
为了避免报错,我们需要将代码修改为:
1 | var log = new Function("var a = '1\\n23', console.log(a)") |
不止 \n ,其他三种 行终结符
,如果你在字符串表达式中直接使用,都会导致报错!
之所以讲这个问题,是因为在模板引擎的实现中,就是使用了 Function 构造函数,如果我们在模板字符串中使用了 行终结符
,便有可能会出现一样的错误,所以我们必须要对这四种 行终结符
进行特殊的处理。
特殊字符
除了这四种行终结符
之外,我们还要对两个字符串进行处理
一个是 \
比如我们在模板内容中使用了 \
1 | var log = new Function("var a = '1\23';console.log(a)"); |
其实我们是想打印 ‘1\23’,但是因为把 \ 当成了特殊字符的标记进行处理,所以最终打印了 1。
同样的道理,如果我们在使用模板引擎的时候,使用了 \ 字符串,也会导致错误的处理。
所以总共我们需要对六种字符进行特殊处理,处理的方式,就是正则匹配出这些特殊字符,然后比如将 \n 替换成 \n,\ 替换成 \,’ 替换成 \‘,处理的代码为:
1 |
|
我们测试一下:
1 | var str = 'console.log("I am \n blue");'; |
replace
这里我们来讲讲 replace函数:
语法:
1 | str.replace(regextp|substr, newSubStr|function) |
replace 的第一个参数,可以传一个字符串,也可以传一个正则表达式。
第二个参数,可以传一个新字符串,也可以传一个函数。
这里重点看传入一个函数的情况
1 | var str = 'hello world'; |
match 表示匹配到的字符串,但函数的参数其实不止有match,下面是个更复杂的例子:
1 | function replacer(match, p1, p2, p3, offset, string) { |
另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用
举个例子
1 | var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>' |
传入的函数会被执行两次,第一次的打印结果为:
1 | <%= www.baidu.com %> |
第二次打印结果为:
1 | <%=baidu%> |
正则表达式的创建
当我们要建立一个正则表达式的时候,我们可以直接创建:
1 | var reg = /ab+c/i; |
也可以使用构造函数的方式:
1 | new RegExp('ab+c', 'i'); |
值得一提的是:每个正则表达式对象都有一个source属性,返回当前正则表达式对象的模式文本的字符串:
1 | var regex = /fooBar/ig |
正则表达式的特殊字符
在underscore的实现中,匹配 <%=xxx%>
使用的是/<%=([\s\S]+?)%>/g
, \s 表示匹配一个空白符,包括空格,制表符,换页符,换行符,和其他 Unicode空格,\S匹配一个非空白符,[\s\S]就表示匹配所有的内容,可是为什么我们不直接使用 . 呢?
我们可能以为 . 匹配任意单个字符,实际上,并不是如此,.
匹配除 行终结符
之外的任何单个字符
1 | var str = '<%=hello world%>' |
但是如果我们在 hello world 之间加上一个行终结符
,比如说\u2029
:
1 | var str = '<%=hello \u2029 world%>' |
因为匹配不到,所以不会执行console.log函数
但是改成/<%=([\s\S]+?)%>/g
就可以正常匹配:
1 | var str = '<%=hello \u2029 world%>' |
惰性匹配
仔细看 /<%=([\s\S]+?)%>/g
这个正则表达式,我们知道 x+ 表示匹配 x 1次或多次,x?表示匹配 x 0次或1次,但是 +? 是什么鬼?
实际上,如果在数量词 *、+、? 或者 {} 任意一个后面紧跟该符号 ?
,会使数量词变为非贪婪(non-greedy)|惰性,即匹配次数最小化。反之,默认情况下,是贪婪的(greedy)|非惰性,即匹配次数最大化。
1 | //非惰性 |
在这里我们使用惰性匹配
1 | var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>' |
如果我们使用非惰性匹配
1 | var str = '<li><a href="<%=www.baidu.com%>"><%=baidu%></a></li>' |
template
所需要的知识点已经过了一遍,下面是underscore模板引擎的实现
与之前使用数组push,最后在用join的方法不同,underscore使用的是字符串拼接的方式。
比如下面这样一段模板字符串:
1 | <%for ( var i = 0; i < users.length; i++ ) { %> |
我们先将 <%=xxx%>
替换成 '+ xxx +'
,在将 <%xxx%>
替换成 '; xxx _p+='
1 | ';for ( var i = 0; i < users.length; i++ ) { __p+=' |
这段代码还不完整,还需要添加头尾代码,组合成一个完整的代码字符串:
1 | var _p=''; |
整理下代码就是:
1 | var __p=''; |
然后我们将 _p 这段代码字符串传入 Function 构造函数中:
1 | var render = new Function(data, _p); |
我们执行这个render函数,传入需要的data数据,就可以返回一段HTML字符串:
1 | render(data); |
第五版 - 特殊字符的处理
这里接着上篇的第四版进行书写,不过加入对特殊字符的转义以及使用字符串拼接的方式:
1 | // 第五版 |
第六版 - 特殊值的处理
如果数据中 users[i].url
不存在怎么办?此时取值的结果为undefined,我们知道
1 | '1' + undefined // "1undefined" |
就相当于拼接了undefined字符串,这肯定不是我们想要的。我们可以在代码中加入一点判断:
1 | .replace(settings.interpolate, function(match, interpolate){ |
这里的 interpolate 有点多
1 | var source = "var __t, __p='';\n"; |
其实就相当于:
1 | var _t; |
第七版
现在我们使用的方式是将模板字符串进行多次替换,然而underscore的实现中,只进行了一次替换,我们来看看underscore是怎么实现的:
1 |
|
其实原理也很简单,就是在执行多次匹配函数的时候,不断复制字符串,处理字符串,拼接字符串,最后拼接首尾代码,得到最终的代码字符串。
不过值得一提的是:在这段代码里,matcher 的表达式最后为:/<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
问题是为什么还要加个 |$ 呢?我们来看下 $:
1 |
|
我们之所以匹配 $,是为了获取最后一个字符串的位置,这样当我们 text.slice(index, offset)的时候,就可以截取到最后一个字符。
最终版
其实代码写到这里,就已经跟 underscore 的实现很接近了,只是 underscore 加入了更多细节的处理,比如:
- 对数据的转义功能
- 可传入配置项
- 对错误的处理
- 添加 source 属性,以方便查看代码字符串
- 添加了方便调试的 print 函数
…1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114// ----- html 代码
<div id="container"></div>
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
//---------js代码
/**
* 模板引擎第八版
*/
var _ = {};
_.templateSettings = {
// 求值
evaluate: /<%([\s\S]+?)%>/g,
// 插入
interpolate: /<%=([\s\S]+?)%>/g,
// 转义
escape: /<%-([\s\S]+?)%>/g
};
var noMatch = /(.)^/;
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
_.template = function(text, settings) {
settings = Object.assign({}, _.templateSettings, settings);
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length;
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
return match;
});
source += "';\n";
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
var render;
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
var template = function(data) {
return render.call(this, data, _);
};
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template;
};
var results = document.getElementById("container");
var data = {
users: [
{ "name": "Byron", "url": "http://localhost" },
{ "name": "Casper", "url": "http://localhost" },
{ "name": "Frank", "url": "http://localhost" }
]
}
var text = document.getElementById("user_tmpl").innerHTML
var compiled = _.template(text);
console.log(compiled.source)
results.innerHTML = compiled(data);