ES6 深入浅出:模板字符串

ES6 深入浅出 是一个系列文章,介绍了在 ECMAScript 标准第 6 版(简称 ES6)中添加的 JavaScript 编程语言的新功能。

上周,我承诺要改变一下节奏。我说,在介绍了 迭代器生成器 之后,我们将着手一些简单的东西。我说了,一些不会让你头疼的东西。我们最后看看我是否能兑现诺言。

现在,让我们从简单的东西开始。

反引号基础

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 的值插入到结果字符串中。这可能会生成一条类似 用户 jorendorff 无权执行曲棍球操作。 的消息(这是真的。我没有曲棍球许可证。)

到目前为止,这仅仅是对 + 运算符的一种更友好的语法,而细节如你所想

  • 模板替换中的代码可以是任何 JavaScript 表达式,因此允许函数调用、算术运算等等。(如果你真的想,你甚至可以在另一个模板字符串中嵌套模板字符串,我称之为 模板始祖。)
  • 如果任一值不是字符串,它将使用通常的规则转换为字符串。例如,如果 action 是一个对象,它的 .toString() 方法将被调用。
  • 如果你需要在模板字符串中写入反引号,你必须使用反斜杠对其进行转义:`\`` 等同于 "`"
  • 同样,如果你需要在模板字符串中包含 ${ 这两个字符,我不知道你在做什么,但你可以使用反斜杠转义任一字符:`write \${ or $\{`

与普通字符串不同,模板字符串可以跨越多行

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

模板字符串中的所有空格,包括换行符和缩进,都会原封不动地包含在输出中。

好的。由于上周的承诺,我觉得有责任保护你的大脑健康。所以,快速提醒一下:从这里开始会有点复杂。你现在可以停止阅读,也许去喝杯咖啡,享受你完整、没有融化的脑子。说真的,没有必要退缩。洛佩斯·冈萨雷斯 在证明船只可以穿越赤道而不会被海怪压碎或掉下地球边缘之后,是否对整个南半球进行了详尽的探索?没有。他转身回家,吃了一顿美餐。你喜欢午餐,对吧?

反引号未来

让我们谈谈模板字符串做的事情。

  • 它们不会自动为你转义特殊字符。为了避免 跨站点脚本 漏洞,你仍然必须小心处理不受信任的数据,就像你在连接普通字符串时一样。
  • 它们如何与 国际化库(一个帮助你的代码对不同的用户说不同语言的库)交互并不明显。模板字符串不处理数字和日期的语言特定格式,更不用说复数了。
  • 它们不能替代模板库,例如 MustacheNunjucks

    模板字符串没有内置的循环语法——例如,从数组构建 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(<var>templateData</var>, bonk.sender);

其中 <var>templateData</var> 是模板所有字符串部分组成的不可变数组,由 JS 引擎为我们创建。这里数组将有两个元素,因为标记模板中有两个字符串部分,由替换隔开。所以 <var>templateData</var> 将类似于 <a href="https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze" target="_blank">Object.freeze</a>(["<p>", " has sent you a bonk.</p>"]

(实际上,<var>templateData</var> 上还存在一个属性。我们不会在本文中使用它,但我会为了完整性而提到它:<var>templateData</var>.raw 是另一个包含标记模板中所有字符串部分的数组,但这次是它们在源代码中看起来的样子——保留了诸如 \n 之类的转义序列,而不是被转换为换行符等等。标准标签 String.raw 使用这些原始字符串。)

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

在继续阅读之前,也许你想试着弄清楚 SaferHTML 应该做什么,然后尝试自己实现它。毕竟,它只是一个函数。你可以在 Firefox 开发者控制台中测试你的工作。

以下是一个可能的答案(也可以 作为 gist 获取)。

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>)向他们发送了 bonk,你的用户也是安全的。无论那是什么意思。

(顺便说一下,如果函数使用 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.
    

    请注意,在这个例子中,nameamount 是 JavaScript,但还有一部分你不熟悉的代码,即 :c(CAD),Jack 将其放置在模板的字符串部分。JavaScript 自然由 JavaScript 引擎处理;字符串部分由 Jack 的 i18n 标签处理。用户将从 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 并与语言的其余部分智能交互。 坦率地说,我想不出任何其他语言中有什么东西能与之匹敌。 我不知道这个特性会把我们带到哪里。 可能性令人兴奋。

我什么时候可以开始使用它?

在服务器端,ES6 模板字符串目前在 io.js 中得到支持。

在浏览器中,Firefox 34 及更高版本支持模板字符串。 它们是由 Guptha Rajagopal 在去年夏天作为实习项目实现的。 模板字符串在 Chrome 41 及更高版本中也得到支持,但在 IE 或 Safari 中不支持。 目前,如果您想在网络上使用模板字符串,则需要使用 BabelTraceur。 您也可以立即在 TypeScript 中使用它们!

等等,那 Markdown 呢?

嗯?

哦,……问得好。

(本节与 JavaScript 关系不大。如果您不使用 Markdown,可以跳过此部分。)

有了模板字符串,Markdown 和 JavaScript 现在都使用 ` 字符来表示特殊含义。 事实上,在 Markdown 中,它是内联文本中间的 code 片段的定界符。

这就带来了一个小问题! 如果您在 Markdown 文档中写下以下内容

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

它将显示为以下内容

要显示消息,请写入 alert(hello world!)

请注意,输出中没有反引号。 Markdown 将所有四个反引号解释为代码定界符,并用 HTML 标签替换了它们。

为了避免这种情况,我们转向 Markdown 从一开始就包含的一个鲜为人知的功能:您可以使用多个反引号作为代码定界符,例如

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

This Gist 包含详细信息,并且是用 Markdown 写成的,因此您可以查看源代码。

接下来

下周,我们将看看程序员在其他语言中享受了几十年的两个特性:一个是对于喜欢尽可能避免争论的人,另一个是对于喜欢争论的人。 我说的是函数参数,当然。 这两个特性实际上是为我们所有人准备的。

我们将通过在 Firefox 中实现这些特性的开发人员的视角来了解这些特性。 所以请在下周加入我们,届时客座作者 Benjamin Peterson 将深入介绍 ES6 默认参数和剩余参数。

关于 Jason Orendorff

更多 Jason Orendorff 的文章…


11 条评论

  1. Brett Zamir

    写得不错,但除了多行功能之外,我认为模板字符串在模板创建方面正在走向倒退。

    由于模板字符串对于标记语言是不可知的,因此无法区分 HTML 标签名称、属性或文本,因此语法高亮器无法以可靠的方式发挥作用。

    我之前在这里提过,但像 JsonML 或我的库 Jamilih 这样的库允许您利用在 JavaScript 中构建 DOM 树(或可能构建 DOM 字符串),同时获得离散的语法高亮,没有 HTML 中难看的括号和闭合标签,但具有 JavaScript 的全部表达能力。(如果不想让设计师拥有这种能力,可以坚持使用 JSON 而不是纯 JavaScript。)

    仅仅因为应该进行关注点分离(到设计逻辑、业务逻辑等)并不意味着需要分离语法。 JavaScript 是唯一可以包含其他语法的语言(是的,您可以将 JavaScript 嵌入到 HTML 模板中,但这需要一些专有语法,这种语法通常不适用于语法高亮,我认为通常会限制您)。

    例如,您给出的模板代码在 Jamilih 中看起来像这样,并且消除了对专有“#for”语法的任何了解

    var libraryHtml = jml(‘ul’, [
    myBooks.map(book => [‘li’, [
    [‘i’, book.title],
    ‘ by ‘ + book.author
    ])
    ];

    May 15th, 2015 at 00:57

    1. Brian Di Palma

      我不建议将字符串模板用于除非常简单的模板用例之外的任何其他目的。 正如 Brett Zamir 已经提到的,使用您已经可以访问的编程语言是更复杂的用例的更好方法。 React 向我们展示了方向,使用代码!

      May 15th, 2015 at 13:08

  2. Gastón I. Silva

    > 带标签的模板为库设计者提供了创建强大特定领域语言的机会。

    我对这完全不兴奋,请把它保密。(嘘!)

    我看到它对 Rails 和 gem 生态系统的影响,我认为它被滥用了。 有很多 gem 声称可以*非常轻松地*为您的应用程序添加新功能。 这只对最初的 15 分钟有效。 然后你会意识到 DSL 的某些部分并不像作者想象的那样直观,或者在某种程度上限制了你。

    我有几次不得不修改这些聪明的 DSL gem,无论是为了自定义目的还是因为存在 bug,而这导致的*浪费*时间比我一开始就使用普通的 Ruby/HTML/JS 更多。

    May 16th, 2015 at 13:11

    1. voracity

      绝对的,DSL 非常强大,但也非常危险。 它们之所以危险,正是因为它们没有像 JavaScript 这样的语言那样经过严格的标准化流程。

      另一个重大担忧是我们可能会开始看到这样的事情(用您最喜欢的语言替换):

      java`
      class HelloWorldApp {
      public static void main(String[] args) {
      System.out.println(“Hello World!”); // Display the string.
      }
      }
      `

      HelloWorldApp.main();

      这当然之前也是可以的,但并不像现在这样干净方便。

      May 17th, 2015 at 20:10

      1. voracity

        为什么地球上的评论系统要剥离标签,而不是正确地转义所有内容? 这更接近于意图

        [script]
        java`
        class HelloWorldApp {
        ____public static void main(String[] args) {
        ________System.out.println(“Hello World!”); // Display the string.
        ____}
        }
        `

        HelloWorldApp.main();
        [script]

        May 17th, 2015 at 20:13

    2. Jason Orendorff

      > 我看到它对 Rails 和 gem 生态系统的影响,我认为它被滥用了。

      也许是这样,也许它也会在 JS 中被滥用。 但同样,也许编程语言中的灵活性和表达能力总体上是件好事,也许我们可以不断改进在明智地使用它们方面做得更好。

      DSL 在 Ruby 的“酷”文化鼎盛时期很“酷”。 事实证明,“酷”不是一个衡量健全工程实践的良好标准。 现在由于它们不再“酷”而拒绝它们,就是重复了选择风格和声誉而不是实质和经验的同样错误。

      一些带标签的模板会非常受欢迎,而且不会引起争议。 我认为那些行为类似于“带有额外格式选项的字符串插值”的标签将属于这一类。“带有少量额外检查的字符串插值”也将非常有用。

      除此之外,我们还看到了什么? Web 开发社区具有令人惊讶和有趣的编译器工程文化:(参见 SASS、LESS、CoffeeScript、sweet.js、Babel 等等)。 将这种经验与带标签的模板结合起来可能会带来巨大的成果。

      但是,我要在此稍微贬低一下我的观点,说我真的很期待滥用它们。 ASCII-To-SVG 作为带标签的模板? 谁不想生活在一个这种事情成为现实的未来?

      May 18th, 2015 at 10:11

      1. voracity

        虽然您的评论不是针对我的,但我意识到我的评论可能会被解释为反对模板字符串和/或 DSL,但事实并非如此。 我非常期待模板字符串和 DSL(狭义 DSL 比通用 DSL 更重要)的创造性、奇怪和奇妙的用途。 从简单的 $`body`(目前无法正常工作 :( )到 jsx`[div][/div]`,再到像 fn`x“y` 这样的链式调用——我不知道什么有用且低摩擦,什么没有用,但找出答案将会很有趣。

        但我认为这项功能应该附带注意事项。 DSL 允许使用通用语言,这些语言可能会取代 JavaScript。 但是 JavaScript(包括 ES6 和 ES7)是一种美丽、持久、强大、多样、创造性、奇怪和奇妙的语言,它从许多其他语言中汲取了优点。(它可以被称为*基于它借鉴了多少语言的脚本*。是的,创建您自己的 DSL,并且要雄心勃勃,但我希望人们不要为了自己对事物“应该”如何运作的严格观点(通常是为了安全、性能、正确性的短期原因;但也为了更合理的原因,例如清晰度)而着手破坏和取代全球的 JavaScript。 我希望我*永远*不会看到用 (java||coffeescript||cpp||c||haskell||python||ruby||rust||go)`…` 编写的整个网站变得司空见惯——尽管我很想看到(并从中学习)这样的网站作为实验。

        May 21st, 2015 at 22:44

  3. PhistucK

    实际上,模板字符串似乎在 Chrome 41 中默认启用。 根据 https://www.chromestatus.com/features/4743002513735680
    即使这是错误的,我也使用的是 Chrome 42,没有使用该标志——它们是可用的。

    May 16th, 2015 at 23:23

    1. Jason Orendorff

      谢谢。 你是对的。 我会修改这篇文章。

      May 18th, 2015 at 10:12

      1. PhistucK

        事实证明,它已经在 Microsoft Edge(新的 Internet Explorer)中实现了——
        http://dev.modern.ie/platform/status/templatestringses6

        因此,新的 Microsoft 浏览器(将于今年夏天与 Windows 10 一起发布)也支持它。

        May 18th, 2015 at 11:03

  4. keed

    它在我们现在的 ES6+ 中如此大胆的语法…

    someFunc(a => {
    “呼!”;
    });

    someFunc(fn () {
    “我希望我们很快就能有这种简短的旧语法 :-)”;
    });

    function* abc() {
    “我的天!这是苏联的开发成果!”;
    }

    generator abc() {
    “我希望我们很快就能有这种语法 :-)”;
    }

    var a = `它看起来像一个 ${crutch},你知道的`;

    var a = “它看起来像一个 \{未来}”;

    Markdown… 是的,是的,这是一个很棒的解决方案,太棒了!我喜欢现代的 JS :-)
    要显示一条消息,请编写“alert(`hello world!`)“

    var {a, …b} = c;
    没有办法修改要导出的 var 的名称。因此我无法创建像 var a = {“return”: “a”} 这样的对象,因为 var {return, …a} = a; 太棒了!

    2015 年 5 月 26 日 下午 8:09

本文的评论已关闭。