underscore 系列之完成一个模板引擎(下)

媒介

本篇接着上篇 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 字符值转义序列寄义
u0009t制表符
u000An换行
u000Dr回车
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 到场了更多细节的处置惩罚,比方:

  1. 对数据的转义功用
  2. 可传入设置项
  3. 对毛病的处置惩罚
  4. 增加 source 属性,以轻易检察代码字符串
  5. 增加了轻易调试的 print 函数

然则这些内容都还算简朴,就不一版一版写了,末了的版本在 template 示例八,假如对个中有疑问,迎接留言议论。

underscore 系列

underscore 系列目次地点:https://github.com/mqyqingfeng/Blog

underscore 系列估计写八篇摆布,重点引见 underscore 中的代码架构、链式挪用、内部函数、模板引擎等内容,旨在协助人人浏览源码,以及写出本身的 undercore。

假如有毛病或许不严谨的处所,请务必赋予斧正,非常谢谢。假如喜好或许有所启示,迎接 star,对作者也是一种勉励。

    原文作者:冴羽
    原文地址: https://segmentfault.com/a/1190000012983693
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞