模板字符串

模板字符串 #

产生原因 #

传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

优化结果 #

基础使用 #

之前写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

模板字符串(template string)增加版字符串,用反引号作为标识,它可以当作普通字符串使用,也可以用来定义多行字符串

使用模板字符串表示多行字符串,所有的空格缩进都会被保留在输出之中。

(function (log, S) {
    // 普通字符串
    log(`In JavaScript '\n' is a line-feed.`);
    // In JavaScript '
    // ' is a line-feed.

    // 多行字符串
    log(`In JavaScript this is
 not legal.`);
    // In JavaScript this is
    // not legal.

    log(`string text line 1
string text line 2`);
    // string text line 1
    // string text line 2

    $('#list').html(`
        <ul>
        <li>first</li>
        <li>second</li>
        </ul>
    `);
})(console.log, String)

使用反引号 #

代码中的模板字符串,都是用反引号表示。

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

(function (log, S) {
    let greeting = `\`Yo\` World!`;
    log(greeting);// `Yo` World!
})(console.log, String)

使用变量 #

模板字符串中嵌入变量,需要将变量名写在${}之中。

(function (log, S) {
    // 字符串中嵌入变量
    let name = "Bob",
        time = "today";
    log(`Hello ${name}, how are you ${time}?`);
    // Hello Bob, how are you today?

    function authorize(user, action) {
        if (!user.hasPrivilege(action)) {
            throw new Error(
            // 传统写法为
            // 'User '
            // + user.name
            // + ' is not authorized to do '
            // + action
            // + '.'
            `User ${user.name} is not authorized to do ${action}.`);
        }
    }
})(console.log, String)

使用表达式 #

简单表达式

模板字符串大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性

(function (log, S) {
    let x = 1;
    let y = 2;

    log(`${x} + ${y} = ${x + y}`);
    // "1 + 2 = 3"

    log(`${x} + ${y * 2} = ${x + y * 2}`);
    // "1 + 4 = 5"

    let obj = {
        x: 1,
        y: 2
    };
    log(`${obj.x + obj.y}`);
    // "3"
})(console.log, String)

调用函数

模板字符串之中还能调用函数

(function (log, S) {
    function fn() {
        return "Hello World";
    }

    log(`foo ${fn()} bar`);
    // foo Hello World bar
})(console.log, String)

字符串变量

由于模板字符串大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出

(function (log, S) {
    let msg = `Hello ${'Melon'}`;
    log(msg);// Hello Melon
})(console.log, String)

需要时执行

如果需要引用模板字符串本身,在需要执行,可以像下面这样写。

(function (log, S) {
    // 写法一
    let str1 = 'return ' + '`Hello ${name}!`';
    let func1 = new Function('name', str1);
    log(func1('Melon')); // "Hello Melon!"

    // 写法二
    let str2 = '(name) => `Hello ${name}!`';
    let func2 = eval.call(null, str2);
    log(func2('Melon')); // "Hello Melon!"
})(console.log, String)

使用对象变量 #

如果大括号中的值不是字符串,将按照一般的规则转为字符串

比如,大括号中是一个对象,将默认调用对象toString方法。

(function (log, S) {
    let a = [1,3,4];
    let o = {melon:1};
    let d = Reflect.construct(Date,[]);

    log(`${a} + ${o} = ${d}`);
    // 1,3,4 + [object Object] = Mon Feb 11 2019 23:45:23 GMT+0800 (GMT+08:00)

})(console.log, String)

使用未声明变量 #

如果模板字符串中的变量没有声明,将报错

(function (log, S) {
    // 变量place没有声明
    let msg = `Hello, ${place}`;// ReferenceError: place is not defined
})(console.log, String)

模板嵌套 #

模板字符串可以嵌套

下面tmpl方法中,模板字符串变量之中,又嵌入了另一个模板字符串

(function (log, S) {
    const tmpl = addrs => `
    <table>
    ${addrs.map(addr => `
      <tr><td>${addr.first}</td></tr>
      <tr><td>${addr.last}</td></tr>
    `).join('')}
    </table>
  `;

    const data = [{
            first: '<Jane>',
            last: 'Bond'
        },
        {
            first: 'Lars',
            last: '<Croft>'
        },
    ];

    log(tmpl(data));
    // <table>
    //
    //   <tr><td><Jane></td></tr>
    //   <tr><td>Bond</td></tr>
    //
    //   <tr><td>Lars</td></tr>
    //   <tr><td><Croft></td></tr>
    //
    // </table>
})(console.log, String)

模板编译 #

准备需要转换的模板 #

const template = `
<ul>
  <% for(let i=0; i < data.supplies.length; i++) { %>
    <li><%= data.supplies[i] %></li>
  <% } %>
</ul>
`;

上面代码在模板字符串之中,放置了一个常规模板

该模板使用<%...%>放置 JavaScript 代码,使用<%= ... %>输出 JavaScript 表达式。


之前转换的方法 #

将其转换为 JavaScript 表达式字符串。

(function (log, S) {
    function tmpl(data) {
        const arr = [];
        arr.push('<ul>');
        for (let i = 0; i < data.supplies.length; i++) {
            arr.push('\n\t<li>');
            arr.push(data.supplies[i]);
            arr.push('</li>');
        };
        arr.push('\n</ul>');
        return arr.join('');
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

正则加echo和eval #

使用正则表达式将模板中<%= ... %>转换为echo字符串拼接方法,然后再结合模板字符串,转译最后的编译方法完整体,最后再调用eval动态执行函数编译

(function (log, S) {
    function compile(template) {
        const evalExpr = /<%=(.+?)%>/g;
        const expr = /<%([\s\S]+?)%>/g;

        template = template.replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')

        template = template.replace(expr, '`); \n $1 \n  echo(`');

        template = 'echo(`' + template + '`);';

        let script =
            `(function parse(data){
          let output = "";

          function echo(html){
            output += html;
          }

          ${ template }

          return output;
        })`;

        return script;
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };

    const temp = `
    <ul>
        <% for(let i=0; i < data.supplies.length; i++) { %>
            <li><%= data.supplies[i] %></li>
        <% } %>
    </ul>
    `;

    // let tmpl = new Function(temp,compile);
    const compileStr = compile(temp);
    log(compileStr);

    // (function parse(data) {
    //     let output = "";

    //     function echo(html) {
    //         output += html;
    //     }

    //     echo(`
    //         <ul>
    //             `);
    //     for (let i = 0; i < data.supplies.length; i++) {
    //         echo(`
    //                 <li>`);
    //         echo(data.supplies[i]);
    //         echo(`</li>
    //             `);
    //     }
    //     echo(`
    //         </ul>
    //         `);

    //     return output;
    // })

    let tmpl = eval(compileStr);
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

正则加数组和eval #

使用正则表达式将模板中<%= ... %>转换为arr.push字符串拼接方法,然后再结合模板字符串,转译最后的编译方法完整体,最后再调用eval动态执行函数编译

(function (log, S) {
    function compile(template) {
        const evalExpr = /<%=(.+?)%>/g;
        const expr = /<%([\s\S]+?)%>/g;

        template = template.replace(evalExpr, '`); \n  arr.push( $1 ); \n  arr.push(`');
        // 替换模板字符串中的<%=...%>中的值  
        // 比如将 '<%= data.supplies[i] %>'
        // 转换为 '`);\n  arr.push( data.supplies[i]); \n  arr.push(`'

        template = template.replace(expr, '`); \n $1 \n  arr.push(`');
        // 替换模板字符串中的<%...%>中的值  
        // 比如将 '<% for(let i=0; i < data.supplies.length; i++) { %>'
        // 转换为 '`);\n  for(let i=0; i < data.supplies.length; i++)  \n  arr.push(`'

        template = 'arr.push(`' + template + '`);'; // 做最外层的包裹

        let script =
            `(function parse(data){
                const arr = [];

                ${ template }

                return arr.join('');
            })`;

        return script;
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };

    const temp = `
    <ul>
        <% for(let i=0; i < data.supplies.length; i++) { %>
            <li><%= data.supplies[i] %></li>
        <% } %>
    </ul>
    `;

    // let tmpl = new Function(temp,compile);
    const compileStr = compile(temp);
    log(compileStr);

    // (function parse(data){
    //     const arr = [];

    //     arr.push(`
    //         <ul>
    //     `);
    //     for(let i=0; i < data.supplies.length; i++) {  
    //         arr.push(`
    //             <li>`);
    //         arr.push(  data.supplies[i]  );
    //         arr.push(`</li>
    //         `);
    //     }  
    //     arr.push(`
    //     </ul>
    // `);

    // return arr.join('');
    // })

    let tmpl = eval(compileStr);
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

模板字符串的限制 #

前面提到标签模板里面,可以内嵌其他语言

但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言

举例来说,标签模板里面可以嵌入 LaTEX 语言。


function latex(strings) {
  // ...
}

let document = latex`
\newcommand{\fun}{\textbf{Fun!}}  // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错

Breve over the h goes \u{h}ere // 报错
`

上面代码中,变量document内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的。

但是 JavaScript 引擎会报错。原因就在于字符串转义

模板字符串会将\u00FF\u{42}当作 Unicode 字符进行转义,所以\unicode解析时报错;

\x56会被当作十六进制字符串转义,所以\xerxes会报错。

也就是说,\u\xLaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。

为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。

如果遇到不合法字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串


function tag(strs) {
  strs[0] === undefined
  strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`

上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined/

但是raw属性依然可以得到原始字符串,因此tag函数还是可以对原字符串进行处理。

注意,这种对字符串转义放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错


let bad = `bad escape sequence: \unicode`; // 报错
Build by Loppo 0.6.16