[ES6深度解析]4:Template strings(模板字符串)

撇号基础知识(`)

ES6引入了一种新的字符串字面量语法,称为模板字符串。它们看起来像普通的字符串,除了使用反勾字符ˋ而不是通常的引号'"。在最简单的情况下,它们实际上只是字符串:

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

但这些字符串被称为“模板字符串”,而不是“没有任何特殊功能,只有反引号的乏味的普通字符串”是有原因的。模板字符串给JavaScript带来了简单的字符串插值。也就是说,它们是将JavaScript值插入字符串的一种漂亮、方便的方法。有无数种方法可以使用它,但最能温暖我心的是那句不起眼的错误信息:

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `User ${user.name} is not authorized to do ${action}.`);
  }
}

在这个例子中,${user.name}${action}被称为模板替换。JavaScript将把值user.nameaction插入到结果字符串中。

到目前为止,这只是+操作符的一个稍微更好的语法,下面的细节是你所期望的:

  • 模板替换中的代码可以是任何JavaScript表达式,因此允许函数调用、算术等。如果你愿意,甚至可以在另一个模板字符串中嵌套一个模板字符串,称之为模板初始化(template inception)
  • 如果一个值都不是字符串,它将使用通常的规则转换为字符串。例如,如果action是一个对象,它的.tostring()方法将被调用。
  • 如果你需要在模板字符串中写一个反勾号,你必须用反斜杠来转义它:"ˋ"相同。
  • 同样,如果你需要在模板字符串中包含两个字符${,你可以用反斜杠:\${$\{转义字符。

与普通字符串不同,模板字符串可以包含多行:

$("#warning").html(`
  <h1>Watch out!</h1>
  <p>Unauthorized hockeying can result in penalties
  of up to ${maxPenalty} minutes.</p>
`);

模板字符串中的所有空格,包括换行符和缩进符,都将一字不差地包含在输出中。

撇号的未来

让我们来谈谈模板字符串不能做的一些事情:

  • 它们不会自动为您转义特殊字符。为了避免跨站点脚本漏洞(cross-site scripting),您仍然必须小心处理不可信的数据,就像连接普通字符串一样。
  • 模板字符串不处理数字和日期的特定语言格式,更不用说复数格式了。
  • 它们不是模板库的替代品,就像MustacheNunjucks
  • 模板字符串没有任何用于循环的内置语法——例如,从数组中构建HTML表table的行<tr>——甚至没有条件语句。

标记模板(tagged templates)

ES6在模板字符串上又做了一个改进,让JS开发人员和库设计人员能够解决这些限制等等。该特性称为标记模板(tagged templates)。

标记模板的语法很简单。它们只是模板字符串,在开始的反引号之前有一个额外的标签。对于我们的第一个例子,标签将是SaferHTML,我们将使用这个标签来尝试解决上面列出的第一个限制:自动转义特殊字符。

注意,SaferHTML不是由ES6标准库提供的。我们将在下面自己实现它。

var message = SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;

这里的标记是单一标识符SaferHTML,但标记也可以是一个属性,如SaferHTML.escape,甚至是一个方法调用,如SaferHTML.escape({unicodeControlCharacters: false})。(更准确地说,任何ES6的MemberExpression或CallExpression都可以作为标记。)

我们看到,不带标记模板字符串是简单字符串连接的简写形式。带标记的模板是其他东西的简写:函数调用

上面的代码相当于:

var message = SaferHTML(templateData, bonk.sender);

其中templateData是模板中所有字符串部分的不可变数组,由JS引擎创建。在这里,数组将有两个元素,因为在标记模板中有两个字符串部分,由,分隔。所以templateData会像Object.freeze(["<p>"," has sent you a bonk.</p>"]

实际上在templateData上还有一个属性。我们不会在本文中使用它,但是为了完整起见,我将提到它:templateData.raw是另一个数组,包含了带标签的模板中的所有字符串部分,但这一次与它们在源代码中看到的完全一样——像\n这样的转义序列保持完整,而不是被转换成换行符等等。标准标记String.raw使用这些原始字符串。

这使得SaferHTML函数可以自由地以百万种可能的方式解释字符串和替换。

在继续阅读之前,您可能想要尝试弄清楚SaferHTML应该做什么,然后尝试自己实现它。毕竟,它只是一个函数。这里有一个可能的答案:

function SaferHTML(templateData) {
  var s = templateData[0]; //templateData是字符串部分
  for (var i = 1; i < arguments.length; i++) {//arguments从第二个参数开始,记录的是模板字符串的插值
    var arg = String(arguments[i]);

    // 转义所有的插值
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 不转义模板字符串的其他部分(除了插值)
    s += templateData[i];
  }
  return s;
}

单个示例不足以说明标记模板的灵活性。让我们回顾一下之前的模板字符串限制列表,看看还可以做些什么:

  • 模板字符串不会自动转义特殊字符。但是正如我们所看到的,有了标记模板,您可以用标记自己解决这个问题。事实上,你可以做得更好。从安全角度来看,我的SaferHTML函数非常弱。HTML中的不同位置有不同的特殊字符,需要以不同的方式转义;SaferHTML并没有对它们全部进行转义。但是通过一些努力,你可以编写一个更智能的SaferHTML函数,它实际解析templateData字符串中的HTML位置,这样它就知道哪些替换是在普通HTML中;哪些是元素属性内部的,因此需要转义'";哪些是在URL查询字符串中,因此需要URL转义而不是html转义;等等。它可以对每个需要替换的字符串执行正确的转义。这听起来是不是有些牵强,因为HTML解析很慢?幸运的是,当再次计算模板时,已标记模板的字符串部分不会更改。SaferHTML可以缓存所有这些解析的结果,以加快以后的调用。
  • 模板字符串没有内置的国际化特性。但是有了标签,我们可以添加它们。注意,在下面的这个例子中,nameamount是JavaScript的变量,但是有一个不熟悉的代码:c(CAD),把它放在了模板的字符串部分。JavaScript当然是由JavaScript引擎处理的;字符串部分由i18n标签函数处理。用户可以从i18n文档中了解到:c(CAD)表示金额是货币的数量,以加元计价。
    这就是标记模板的作用。
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.
  • 模板字符串不能替代MustacheNunjucks,部分原因是它们没有用于循环或条件句的内置语法。但现在我们开始考虑如何解决这个问题了,对吧?如果JS不提供该特性,那么编写一个提供该特性的标记。
// 纯粹假设的模板语言(基于ES6的标记模板)
var libraryHtml = hashTemplate`
  <ul>
    #for book in ${myBooks}
      <li><i>#{book.title}</i> by #{book.author}</li>
    #end
  </ul>
`;

灵活性并不止于此。注意,标签函数的参数不会自动转换为字符串。它们可以是任何东西。返回值也是如此。带标签的模板甚至不一定是字符串!你可以使用自定义标签来创建正则表达式、DOM树、图像、代表整个异步进程的承诺、JS数据结构、GL着色器……

posted @ 2021-08-20 21:35  Max力出奇迹  阅读(540)  评论(0编辑  收藏  举报
返回顶部↑