ES6 in Depth: Template Strings

ES6 in Depth: Template strings

上周,我们已经提到要变步伐。在迭代器和生成器之后,我们来开始些简单的,这是我之前说过的。我也说过,这些是你未曾遇到过的。那,我们来看这承诺是否能够保持吧。

现在,让我们开始些简单的。

音符基础

ES6 推出了一种新的字符串语法,叫做模板字符串。他们看起来很像普通的字符串,除了其使用了音符式的字符,而不仅仅是'"这些引号字符。对于最简单的情况,他们就是字符串:

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

但是,之所以称为模板字符串,而不是又普通又老的、仅包含音符的字符串,是因为模板字符串为JS提供了字符串的插入操作。这为JS提供了种好看、且方便的方法来将值插入到字符串中。

错误提示可以有上千百万的方法来实现,但是下面是我内心比较喜欢的:

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

在这个例子中,${user.name}${action}叫做 模板替换。JS 会将user.nameaction的值插入到最终的字符串中。这可能会产生一个消息如:User jorendorff is not authorized to do hockey.

目前,这相对于+的拼接操作更显得优雅点,你可能还会希望其详细如下:

  • 代码中的模板替换可以是任意的JS表达式,如 函数调用、算法等等都是允许的。(或许,你甚至还希望能在一个模板字符串中肉插入另一个模板字符串,我称为模板插入。)

  • 如果值不是字符串时,它会使用常用的规则转为字符串。例如,如果action是个对象,那么.toString()方法将会被调用。

  • 如果你需要在模板字符串中写入音符号,你必须避免直接用,而是使用斜杠:\',其会为”’”。

  • 同样的,如果你在模板字符串中需要包含${这两个字符,我不知道你想怎么实现,但是你可以使用write \${$\{来规避。

不像普通的字符串,模板字符串可以进行多行:

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

所有模板字符串中的空字符,包括换行符和缩进,都会逐个输出的。

好了。由于我之前在上周的承诺,我感到对你们的大脑需要负一定的责任。所以,先在这说明:我心里有些许的紧张。你现在可以停止往下读,或者应该拿着一杯咖啡、好好享受一下完整而未被融化的大脑。真的,返回去并不丢脸。当 Lopes Goncalves 得到一艘海里怪兽攻击、不被暗礁打翻的船后,他是不是探索完了整个南半球?不,他返回,回家了,然后还吃了顿可口的午餐。你喜欢吃午餐,对不对?

音符的未来

让我们讨论一下新的东西,模板字符串不做什么:

  • 它们不会自动帮你处理特殊字符。为了避免跨域网站的脚本攻击,你将不得不小心地处理非信任的数据,就如你拼接普通的字符串。

  • 它不直接作用于国际化库(对于不同的用户使用不同的语言库)。模板字符串不会处理特定语言的数字和日期的格式,更不用说复数了。

  • 它们并不是模板库的替代品,如 Mustanche 和 Nunjucks。模板字符串没有任何内置的循环语法–例如,利用一数组来构建HTML中table的多行,或者条件。(是的,你可以使用模板插入来做到这目的,但对我来说,你那样做看起来就是是排序,是个笑话。)

ES6 为模板字符串不仅仅提供了一种方法,其可让JS开发人员和库的设计人员更多的能量来解决这些限制或者更多。这一特性特为 标签模板

标签模板的语法很简单,它只是在模板字符串打开重音符之前加入额外的标签,这个标签是SaferHTML,是合法的html字符。然后,我们就可以使用这个标签来解决之前的第一条的限制:自动处理特殊字符。

注意,SaferHTML并不是ES6标准库中的某些东西,而是要我们自己来实现的,

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

这标签有个简单的身份:SaferHTML,但是一个标签也可以是一个属性,例如 SaferHTML.escape,或者甚至是一个方法调用,如SaterHTML.escape({unicodeControlCharacters:false})。(准确来说,任何的 MemberExpression or CallExpression 都可以作为一个标签。)

我们已经看到,非标签的模板字符串对于简单的字符串拼接是比较快捷的,标签模板对于某些更为便捷的是:函数调用。

之前的代码等价于:

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

templateData是一个不可变的数组,它包括了由JS引擎替我们生成的模板所有字符串的各部分。这里,这个数组有两个元素,因为这两部分是由替代的标签来分隔的,所以templateData会如Object.freeze(["<p>"," has seet you a bonk.</p>"])

(实际上,还有一个额外的属性出现在templateData中。我们不会在此文章中使用到,但是,为了完整性,我将进行些说明:templdateData.raw是标签模板中的另外一个包含所有字符串各部分的数组,它看起来像代码中在每部分的左边增加多类如\n的符号,而不直接另外换行,等等。标准的标签 String.raw 就是使用这种字符串。)

这里给了方便的SaferHTML方法来解释字上百万种的符串和替代物。

阅读了以上部分时,你也许会对SaferHTML做什么感到困扰,然后,你试着来实现它吧。毕竟,这只是一个函数而已。你可以在 Firefox 的开发者控制台进行调试。

这里,可能的一个答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SaferHTML(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
     var arg = String(arguments[i]);

     // Escape special characters in the substitution.
     s += arg.replace(/&/g, "&amp;")
         .replace(/</g, "&lt;")
         .replace(/>/g, "&gt;");

     // Don't escape special characters in the template.
     s += templateData[i];
  }
  return s;
}

通过这个定义,标签模板SaferHTML <p>${bonk.sender} has sent you a bonk.</p> 会输出为<p>ES6&lt;3er has sent you a bonk.</p>(这结果怎么看都不对啊,作者怎么玩的)。你用户在使用时它都是安全的,即使用户是恶意的用户,如Hacker Steve <script>alert('xss');</script>这样发送一个炸弹。无论代表什么意思。

(顺便提一下,如果函数使用 arguments 对象 来攻击你时确实是个问题,下周会解决掉。这是ES6的另一个新特性,我想你会喜欢的。)

一个简单的例子是不能够说明标签模板其灵活性的,让我们重温一下之前我的提到的模板字符串的短板吧,而现在又可以怎么做:

  • 模板字符串不能自动过滤特殊字符,但是,就如我们看到的,利用标签模板,你使用一个标签来解决这一问题。

实际上,你可以比上面做得更好。

从安全的角度上看,我的SaferHTML函数是十分脆弱的。在HTML中,不同的地方有不同的特殊字符,需求不同的过滤方式,SaferHTML并不能过滤掉所有。但是,通过努力,你可以编写更为智能的SafterHTML函数来对templdateData字符串中的部分HTML,这样子可以让其知道在HTML中哪个是替代物;哪些是内置的元素属性,需要过滤'";哪些是URL查询字符串,需要用URL过滤,而不是HTML过滤,等等。这样子,才能正确处理好每个替代物。

会不会由于HTML的解释速度慢而使目标难到达呢?幸运的是,当标签模板重新执行时,其字符串块并不会改变。SafteHTML 可以将解释结果进行缓存,以提升之后调用的速度。(这缓存可以是 WeakMap, 这是另外的ES6特性,我们将会在以后的文章进行讨论。)

  • 模板字符串没有内置的国际化特性。但是,利用标签,我们可以实现。文章 展示了怎么实现的第一步。下面是个例子,可当作小玩意:
1
2
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.

注意,现在这个例子,nameamount是属于Javascript,但是这里又有些不同的不熟悉的代码,那就是:c(CAD),这就是模板中需要替换的字符串部分。Javascript部分由JS的引擎控制,那个字符串部分由 Jack 的 i18n 的标签来控制。用户可以从 i18n 的文档中知道 :c(CAD)表示amount数量的货币,叫做加元。

这就是标签模板的作用。

  • 模板字符串不是Mustache和Nunjucks的替代品,一定程度上是因为它没有内置的循环或条件的语法。但是现在,让我们看看你可以怎么实现它。如果JS没有提供这一特性,那么可以写一个标签来达到这目的。
1
2
3
4
5
6
7
8
9
// Purely hypothetical template language based on
// ES6 tagged templates.
var libraryHtml = hashTemplate`
  <ul>
     #for book in ${myBooks}
       <li><i>#{book.title}</i> by #{book.author}</li>
     #end
  </ul>
`;

其灵活性不止于此。注意,标签函数中的参数并不会自动转为字符串,它们可以是任何类型,返回值也是一样。标签模板甚至并不必要是字符串!你可以使用自定义的标签来实现一定的表达式,DOM 树、图片、代表全部异步进程的Promise、JS的数据结构、GL着色器(GL shaders)…

标签模板欢迎库设计者来创建强大的领域语言。这语言可能看起来并不像JS,但是它们可以无缝地嵌于JS中,并智能地与其它语言相互影响。不负责地说,我不能想象有其它语言可以做到这点。我说不出这特性会带给我们什么,这可能性却是令人兴奋的。

什么时候我可能开始使用?

在服务端,io.js现在已经支持ES6的模板字符串了。

在浏览器,Firefox 34+ 支持模板字符串,它由Guptha Rajagopal 在去年夏天的一个内部项目中实现的。模板字符串还得到 Chrome 41+ 的支持,但是不是包括 IE 和 Safari。现在,如果你需要在网站中使用模板字符串时,你将需要 Babel 或者 Traceur (来转化为ES5)。你现在还可以使用 TypeScript

什么是Markdown?

Hmm?

好问题。

(这部分并不真正与JS有关,如果你不使用Markdown,你可以跳过这部分。)

对于模板字符串,Markdown和JS现在都使用 ` 符号来代码特殊含义。实际上,在Markdown中,它是内容中的代码界定符号。

如果你写以下Markdown文档时,这会带来些问题:

1
To display a message, write `alert(`hello world!`)`.

这会显示为:

To display a message, write `alert(`hello world!`)`.

注意,这里并没有符号输出,Markdown 解释器会将四个空格认为是代码的界定符号,然后使用HTML的标签来替换它。

为了避免这问题,我们在开始时了解到Markdown些特性:你可以多个符号来作为代码的界定符号,如:

1
To display a message, write ``alert(`hello world!`)``.

Gist 有更为详细的内容,它是用Markdown来编写的,你可以看到其源码。

下一步

下周,我们将着手这数十年来程序大大在其它语言十分乐道的两个特性:一、大家喜欢尽可能不写没必要的参数;二、部分人喜欢很多的参数。好吧,我说的是就是函数的参数,这特性是我们所有人都需要知道的。

我们将通过 Firefox中这一特性的作者的角度来了解这些特性,所以,请在下周加入我们吧,猜测一下,其作者 Benjamin Peterson 是怎么深入 ES6的默认参数和剩余参数。

Comments