返回首页 深度解析 ECMAScript 6

模版字符串

反引号的基础知识

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.name 和 action 的值放置到结果字符串中。这可能会产生类似于用户 jorendorff 无权玩曲棍球这样的消息。 (这是真的。我没有曲棍球许可。)

到目前为止,这仅仅是一个为 + 运算稍有更好的语法。你可能期望的更多一些的细节:

  • 模块字符串中的代码可以是任何 JavaScript 的表达式,所以函数调用,算术等操作都是允许的。(如果你真的愿意,你甚至可以嵌套一个模版字符串于另一个模板字符串中,我称之为 template inception。)

  • 如果一个值不是一个字符串,则按照一般规则将其转换为字符串。例如,如果 action 是一个对象,它的 .toString() 方法将被调用。

  • 如果你需要在一个字符串模板内写反引号,你必须在其前面用反斜杠:"\ ` " 这与字符串中使用 “ ' ” 的方法是一样的。

  • 同样,如果你需要 在模板中的字符串中包括这两个字符 $ {,虽然我不知道你为什么需要这么做,但你可以用反斜杠来进行转义:写成 ` \${ or ${\ `。

与普通的字符串不同,模版字符串能够写成多行:

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

在模版字符串中所有的空格,包括新行和缩进,在输出中都能一字不差的输出。

好了,因为我上周的承诺,我对你们头脑的健康负有责任。所以,快速的给你们一个警告:从这里开始,事情将会变得有点紧张。你现在可以停止阅读了,你可以先去喝杯咖啡并享受下你的未受损害且还没变糊涂的脑袋。真的,走回头路也不是什么羞耻的事。在证明了船舶能穿越赤道而不会被海怪破坏,或是从地球的边缘掉下去后,Lopes Gonçalves 继续探索了整个南半球了吗?不,他回头了。他回家了,并吃了一顿不错的午餐。你也很喜欢吃午饭吧?

反引号的未来

让我们聊一些模版字符串不能做的东西。

  • 他们不能为您自动转义特殊字符。为了避免跨站点脚本漏洞,你仍然必须仔细地处理不可信数据,就像你串联普通的字符串一样。
  • 他们就如何与国际化库进行交互(一个帮助你的代码对不同的用户说不同的语言)并没明显定义。模板字符串不处理语言特定格式的数字和日期,更不用说复数了。
  • 他们不是模版库的替代品,比如 Mustache 或者 Nunjucks。
    模版字符串没有循环的内置语法。举例来说,从一个数组中创建一个 HTML 表格的行。模版字符串甚至没有条件语句。(是的,你在这里可以使用模版开始,但是对我而言,如果你这么做的话,这看起来像是一个笑话。)

ES6 在模板字符串上提供一个额外的功具使得 JS 开发人员和库设计人员可以解决这些限制。这种功能被称为标签模板。

标签模板的语法很简单。他们只是在开反引号之前加上一个额外的标签的模板字符串。在我们的第一个例子中,该标签是 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];
  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 不进行转义。但通过一些努力,你可以写一个比较聪明的 SaferHTML 函数,使其能实际上能解析在 templateData 里的字符串中的 HTML 块。实现以后,它知道哪些替换是纯 HTML; 哪些是内部元素的属性,需要转义 ' 和 ” ;哪些是在 URL 查询字符串,需要 URL 转义而不是 HTML 转义,等等。这可以只是为每个替换进行转义。

    因为 HTML 解析是很慢的,所以这听起来有些牵强吗?幸运的是,一个标签模板的字符串部分在模版被重新评估时是不改变的。SaferHTML 可以缓存所有分析的结果以加快之后调用。(缓存可能是一个 WeakMap。这是 ES6 的另一个功能,我们将在以后的博客中进行讨论)。

  • 模板字符串没有内置的国际化功能。但通过使用标签,我们将他们添加进去。Jack Hsu 的一个博客帖子展示了这个方法的第一个步骤可能是什么样子的。仅举一例:

      i18n\`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`   
      // => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.

    注意在这个例子中,name 和 amount 是属于 JavaScript 的,但不熟悉的代码有一个不同的位,即:c(CAD),在模板的字符串部分 Jack Hsu 所放的地方。JavaScript 是理所当然由 JavaScript 引擎处理;字符串部分由 Jack Hsu 的 i18n 标签处理。用户将从国际化文档中学习到 :c(CAD) 指的是 amount 是用加元结算的货币的量。

    这就是标签模板所做的工作。

  • 模板字符串是没有替代 Mustache 和 Nunjucks,其部分原因是因为他们没有针对循环或条件的内置语法。但是,现在我们已经开始看到你将如何去解决这个问题了,对吧?如果 JS 在未来不提供该功能,那就写一个标签以提供这种功能。

      // 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 树,图像,承诺代表整个异步进程,JS 数据结构,GL 着色器等等。

    标签模板邀请库设计人员去创建强大的特定领域的语言。这些语言可能看起来一点也不像 JS ,但他们仍然可以嵌入在 JS 中,并与语言的其他部分无缝地智能地进行交互。随口说说,我想不出在任何其他语言有什么与他很类似的。我不知道这个功能将带我们走多远。但是这种可能性是令人兴奋的。

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

在服务器中, io.js 是支持 ES6 模版字符串的。

在浏览器中,火狐浏览器 34 以上的版本是支持模版字符串的。他们是由 Guptha Rajagopal 在上个夏天作为一个实习项目完成的。谷歌浏览器41以上的版本也支持模版字符串,但是微软浏览器 IE 和苹果的浏览器 Safari 不支持。到现在为止,如果你想在网络上使用模版字符串,你将需要使用Babel 或者 Traceur。你也可以在 TypeScript 中立即进行使用。

等等——能不能在 Markdown 中使用?

嗯?

哦,很好的问题。

(这个章节不是真正关于 JavaScript 的。如果你不使用 Markdown,你可以跳过。)

对于模版字符串,Markdown 和 JavaScript 都使用 ` 字符来表明一些特殊性。事实上,在 Markdown 里,它是嵌入式文本中间的代码片段的分隔符。

这带来了点问题!如果你在 Markdown 文档中写这个字符串:

为了显示一条消息,写 ` alert(`世界你好!`)`。

它会显示如下:

<a>为了显示一条消息,写 alert ( 世界你好!)。</a>

注意到,在输出中是没有反引号的。Markdown 将四个反引号都解释为代码分隔符并被 HTML 的标签取而代之。

为了防止这种现象,

我们转向 Markdown 中从一开始就鲜为人知的功能:您可以使用多个反引号作为分隔符的代码,就像这样:

显示一条消息,写``alert( `世界你好!`)` `。

Gist 包含了更多的内容,且其是在 Markdown 中编写,所以你可以去看看。

上一篇: 生成器 下一篇: 非结构化赋值