转义序列
在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);