• ES6 入门教程 4 字符串的扩展 4.6 实例:模板编译 & 4.7 标签模板 & 4.8 模板字符串的限制


    ES6 入门教程

    ECMAScript 6 入门

    作者:阮一峰

    本文仅用于学习记录,不存在任何商业用途,如侵删

    4 字符串的扩展

    4.6 实例:模板编译

    看一个通过模板字符串,生成正式模板的实例。

    let template = `
    
      <% for(let i=0; i < data.supplies.length; i++) { %>
    • <%= data.supplies[i] %>
    • <% } %>
    `
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    【如何编译?】

    一种思路是将其转换为 JavaScript 表达式字符串。

    echo('
      '); for(let i=0; i < data.supplies.length; i++) { echo('
    • '); echo(data.supplies[i]); echo('
    • '
      ); }; echo('
    '
    );
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个转换使用正则表达式就行了。

    let evalExpr = /<%=(.+?)%>/g;
    let expr = /<%([\s\S]+?)%>/g;
    
    template = template
      .replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')
      .replace(expr, '`); \n $1 \n  echo(`');
    
    template = 'echo(`' + template + '`);';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后,将template封装在一个函数里面返回,就可以了。

    let script =
    `(function parse(data){
      let output = "";
    
      function echo(html){
        output += html;
      }
    
      ${ template }
    
      return output;
    })`;
    
    return script;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    将上面的内容拼装成一个模板编译函数compile

    function compile(template){
      const evalExpr = /<%=(.+?)%>/g;
      const expr = /<%([\s\S]+?)%>/g;
    
      template = template
        .replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')
        .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;
    }
    
    • 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

    compile函数的用法如下。

    let parse = eval(compile(template));
    div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
    //   
      //
    • broom
    • //
    • mop
    • //
    • cleaner
    • //
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    4.7 标签模板

    模板字符串的功能,不仅仅是上面这些。

    它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

    alert`hello`
    // 等同于
    alert(['hello'])
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

    但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

    let a = 5;
    let b = 10;
    
    tag`Hello ${ a + b } world ${ a * b }`;
    // 等同于
    tag(['Hello ', ' world ', ''], 15, 50);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面代码中,模板字符串前面有一个标识名tag,它是一个函数。

    整个表达式的返回值,就是tag函数处理模板字符串后的返回值。

    函数tag依次会接收到多个参数。

    function tag(stringArr, value1, value2){
      // ...
    }
    
    // 等同于
    
    function tag(stringArr, ...values){
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。

    tag函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag会接受到value1value2两个参数。

    tag函数所有参数的实际值如下。

    • 第一个参数:['Hello ', ' world ', '']
    • 第二个参数: 15
    • 第三个参数:50

    也就是说,tag函数实际上以下面的形式调用。

    tag(['Hello ', ' world ', ''], 15, 50)
    
    • 1

    可以按照需要编写tag函数的代码。下面是tag函数的一种写法,以及运行结果。

    let a = 5;
    let b = 10;
    
    function tag(s, v1, v2) {
      console.log(s[0]);
      console.log(s[1]);
      console.log(s[2]);
      console.log(v1);
      console.log(v2);
    
      return "OK";
    }
    
    tag`Hello ${ a + b } world ${ a * b}`;
    // "Hello "
    // " world "
    // ""
    // 15
    // 50
    // "OK"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    下面是一个更复杂的例子。

    let total = 30;
    let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
    
    function passthru(literals) {
      let result = '';
      let i = 0;
    
      while (i < literals.length) {
        result += literals[i++];
        if (i < arguments.length) {
          result += arguments[i];
        }
      }
    
      return result;
    }
    
    msg // "The total is 30 (31.5 with tax)"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。

    passthru函数采用 rest 参数的写法如下。

    function passthru(literals, ...values) {
      let output = "";
      let index;
      for (index = 0; index < values.length; index++) {
        output += literals[index] + values[index];
      }
    
      output += literals[index]
      return output;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    “标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。

    let message =
      SaferHTML`

    ${sender} has sent you a message.

    `
    ; function SaferHTML(templateData) { let s = templateData[0]; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]); // Escape special characters in the substitution. s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); // Don't escape special characters in the template. s += templateData[i]; } return s; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。

    let sender = ''; // 恶意代码
    let message = SaferHTML`

    ${sender} has sent you a message.

    `
    ; message //

    <script>alert("abc")</script> has sent you a message.

    • 1
    • 2
    • 3
    • 4
    • 5

    标签模板的另一个应用,就是多语言转换(国际化处理)。

    i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
    // "欢迎访问xxx,您是第xxxx位访问者!"
    
    • 1
    • 2

    模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,可以自己添加这些功能。

    // 下面的hashTemplate函数
    // 是一个自定义的模板处理函数
    let libraryHtml = hashTemplate`
      
      #for book in ${myBooks}
    • #{book.title} by #{book.author}
    • #end
    `
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    除此之外,开发者甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言。

    jsx`
      
    ${this.state.value}
    `
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码通过jsx函数,将一个 DOM 字符串转为 React 对象。可以在 GitHub 找到jsx函数的具体实现。

    一个假想的例子,通过java函数,在 JavaScript 代码之中运行 Java 代码。

    java`
    class HelloWorldApp {
      public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
      }
    }
    `
    HelloWorldApp.main();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    模板处理函数的第一个参数(模板字符串数组),还有一个raw属性。

    console.log`123`
    // ["123", raw: Array[1]]
    
    • 1
    • 2

    上面代码中,console.log接受的参数,实际上是一个数组。

    该数组有一个raw属性,保存的是转义后的原字符串。

    看下面的例子。

    tag`First line\nSecond line`
    
    function tag(strings) {
      console.log(strings.raw[0]);
      // strings.raw[0] 为 "First line\\nSecond line"
      // 打印输出 "First line\nSecond line"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面代码中,tag函数的第一个参数strings,有一个raw属性,也指向一个数组。

    该数组的成员与strings数组完全一致。

    比如,strings数组是["First line\nSecond line"],那么strings.raw数组就是["First line\\nSecond line"]。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw 数组会将\n视为\\n两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。

    4.8 模板字符串的限制

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

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

    举例来说,标签模板里面可以嵌入 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 // 报错
    `
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面代码中,变量document内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。

    模板字符串会将\u00FF\u{42}当作 Unicode 字符进行转义,所以\unicode解析时报错;而\x56会被当作十六进制字符串转义,所以\xerxes会报错。也就是说,\u\x在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。

    为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

    function tag(strs) {
      strs[0] === undefined
      strs.raw[0] === "\\unicode and \\u{55}";
    }
    tag`\unicode and \u{55}`
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined,但是raw属性依然可以得到原始字符串,因此tag函数还是可以对原字符串进行处理。

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

    let bad = `bad escape sequence: \unicode`; // 报错
    
    • 1
  • 相关阅读:
    【css面试题】 实现一个盒子的水平竖直居中对齐效果
    @所有燃气企业,城燃企业数字化转型重点抓住的八个关键点
    java数据结构与算法刷题-----LeetCode104:二叉树的最大深度
    ChatGPT笔记
    K8S:本质
    SpringMVC入门宝典(六)SpringMVC文件上传(上)
    CTR特征建模:ContextNet & MaskNet(Twitter在用的排序模型)
    C++信息学奥赛1181:整数奇偶排序
    【Android】2、Android开发进阶超详细介绍
    nacos
  • 原文地址:https://blog.csdn.net/weixin_44226181/article/details/127840739