(四):模板字符串
最后更新于:2022-04-01 23:35:50
> 原文出处:http://www.infoq.com/cn/articles/es6-in-depth-template-string
在上一篇文章中,我说过要写一篇风格迥异的新文章,在了解了迭代器和生成器后,是时候来品味一些不烧脑的简单知识,如果你们觉得太难了,还不快去啃犀牛书!
现在,就让我们从最简单的知识学起吧!
[TOC]
## 反撇号(`)基础知识
ES6引入了一种新型的字符串字面量语法,我们称之为模板字符串(template strings)。除了使用反撇号字符 ` 代替普通字符串的引号 ' 或 " 外,它们看起来与普通字符串并无二致。在最简单的情况下,它们与普通字符串的表现一致:
~~~
context.fillText(`Ceci n'est pas une chaîne.`, x, y);
~~~
但是我们并没有说:“原来只是被反撇号括起来的普通字符串啊”。模板字符串名之有理,它为JavaScript提供了简单的[字符串插值](https://en.wikipedia.org/wiki/String_interpolation)功能,从此以后,你可以通过一种更加美观、更加方便的方式向字符串中插值了。
模板字符串的使用方式成千上万,但是最让我会心一暖的是将其应用于毫不起眼的错误消息提示:
~~~
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
`用户 ${user.name} 未被授权执行 ${action} 操作。`);
}
}
~~~
在这个示例中,${user.name}和${action}被称为模板占位符,JavaScript将把user.name和action的值插入到最终生成的字符串中,例如:用户jorendorff未被授权打冰球。(这是真的,我还没有获得冰球许可证。)
到目前为止,我们所了解到的仅仅是比 + 运算符更优雅的语法,下面是你可能期待的一些特性细节:
* 模板占位符中的代码可以是任意JavaScript表达式,所以函数调用、算数运算等这些都可以作为占位符使用,你甚至可以在一个模板字符串中嵌套另一个,我称之为模板套构(template inception)。
* 如果这两个值都不是字符串,可以按照常规将其转换为字符串。例如:如果action是一个对象,将会调用它的.toString()方法将其转换为字符串值。
* 如果你需要在模板字符串中书写反撇号,你必须使用反斜杠将其转义:`\``等价于"`"。
* 同样地,如果你需要在模板字符串中引入字符$和{。无论你要实现什么样的目标,你都需要用反斜杠转义每一个字符:`\$`和`\{`。
与普通字符串不同的是,模板字符串可以多行书写:
~~~
$("#warning").html(`
';
小心!>/h1>
未经授权打冰球可能受罚 将近${maxPenalty}分钟。
`); ~~~ 模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。 好啦,我说过要让你们轻松掌握模板字符串,从现在起难度会加大,你可以到此为止,去喝一杯咖啡,慢慢消化之前的知识。真的,及时回头不是一件令人感到羞愧的事情。[Lopes Gonçalves](https://en.wikipedia.org/wiki/Lopes_Gon%C3%A7alves)曾经向我们证明过,船只不会被海妖碾压,也不会从地球的边缘坠落下去,他最终跨越了赤道,但是他有继续探索整个南半球么?并没有,他回家了,吃了一顿丰盛的午餐,你一定不排斥这样的感觉。 ## 反撇号的未来 如粪土让他 当然,模板字符串也并非事事包揽: * 它们不会为你自动转义特殊字符,为了避免[跨站脚本](http://www.techrepublic.com/blog/it-security/what-is-cross-site-scripting/)漏洞,你应当像拼接普通字符串时做的那样对非置信数据进行特殊处理。 * 它们无法很好地与[国际化库](http://yuilibrary.com/yui/docs/intl/)(可以帮助你面向不同用户提供不同的语言)相配合,模板字符串不会格式化特定语言的数字和日期,更别提同时使用不同语言的情况了。 * 它们不能替代模板引擎的地位,例如:[Mustache](https://mustache.github.io/)、[Nunjucks](https://mozilla.github.io/nunjucks/)。 模板字符串没有内建循环语法,所以你无法通过遍历数组来构建类似HTML中的表格,甚至它连条件语句都不支持。你当然可以使用模板套构(template inception)的方法实现,但在我看来这方法略显愚钝啊。 不过,ES6为JS开发者和库设计者提供了一个很好的衍生工具,你可以借助这一特性突破模板字符串的诸多限制,我们称之为标签模板(tagged templates)。 标签模板的语法非常简单,在模板字符串开始的反撇号前附加一个额外的标签即可。我们的第一个示例将添加一个SaferHTML标签,我们要用这个标签来解决上述的第一个限制:自动转义特殊字符。 请注意,ES6标准库不提供类似SaferHTML功能,我们将在下面自己来实现这个功能。 ~~~ var message = SaferHTML`${bonk.sender} 向你示好。
`; ~~~ 这里用到的标签是一个标识符SaferHTML;也可以使用属性值作为标签,例如:SaferHTML.escape;还可以是一个方法调用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精确地说,任何ES6的[成员表达式(MemberExpression)或调用表达式(CallExpression)](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-left-hand-side-expressions)都可作为标签使用。 可以看出,无标签模板字符串简化了简单字符串拼接,标签模板则完全简化了函数调用! 上面的代码等效于: ~~~ var message = SaferHTML(templateData, bonk.sender); ~~~ templateData是一个不可变数组,存储着模板所有的字符串部分,由JS引擎为我们创建。因为占位符将标签模板分割为两个字符串的部分,所以这个数组内含两个元素,形如[Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)(["", " has sent you a bonk."]。 (事实上,templateData中还有一个属性,在这篇文章中我们不会用到,但是它是标签模板不可分割的一环:templateData.raw,它同样是一个数组,存储着标签模板中所有的字符串部分,如果我们查看源码将会发现,在这里是使用形如\n的转义序列分行,而在templateData中则为真正的新行,标准标签String.raw会用到这些原生字符串。) 如此一来,SaferHTML函数就可以有成千上万种方法来解析字符串和占位符。 在继续阅读以前,可能你苦苦思索到底用SaferHTML来做什么,然后着手尝试去实现它,归根结底,它只是一个函数,你可以在Firefox的开发者控制台里测试你的成果。 以下是一种可行的方案(在[gist](https://gist.github.com/jorendorff/1a17f69dbfaafa2304f0)中查看): ~~~ function SaferHTML(templateData) { var s = templateData[0]; for (var i = 1; i < arguments.length; i++) { var arg = String(arguments[i]); // 转义占位符中的特殊字符。 s += arg.replace(/&/g, "&") .replace(/"); // 不转义模板中的特殊字符。 s += templateData[i]; } return s; } ~~~ 通过这样的定义,标签模板SaferHTML`${bonk.sender} 向你示好。
` 可能扩展为字符串 "ES6"。即使一个恶意命名的用户,例如“黑客Stevealert('xss');”,向其他用户发送一条骚扰信息,无论如何这条信息都会被转义为普通字符串,其他用户不会受到潜在攻击的威胁。 (顺便一提,如果你感觉上述代码中在函数内部使用[参数对象](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments)的方式令你感到枯燥乏味,不妨期待下一篇大作,ES6中的另一个新特性一定会让你眼前一亮!) 仅一个简单的示例不足以说明标签模板的灵活性,我们一起回顾下我们之前有关模板字符串限制的列表,看一下你还能做些什么不一样的事情。 * 模板字符串不会自动转义特殊字符。但是正如我们看到的那样,通过标签模板,你可以自己写一个标签函数来解决这个问题。 事实上,你可以做的比那更好。 站在安全角度来说,我实现的SaferHTML函数相当脆弱,你需要通过多种不同的方式将HTML不同部分的特殊字符转义,SaferHTML就无法做到全部转义。但是稍加努力,你就可以写出一个更加智能的SaferHTML函数,它可以针对templateData中字符串中的HTML位进行解析,分析出哪一个占位符是纯HTML;哪一个是元素内部属性,需要转义'和";哪一个是URL的query字符串,需要进行URL转义而非HTML转义,等等。智能SaferHTML函数可以将每个占位符都正确转义。 HTML的解析速度很慢,这种方法听起来是否略显牵强?幸运的是,当模板重新求值的时候标签模板的字符串部分是不改变的。SaferHTML可以缓存所有的解析结果,来加速后续的调用。(缓存可以按照ES6的另一个特性——WeakMap的形式进行存储,我们将在未来的文章中继续深入讨论。) * 模板字符串没有内建的国际化特性,但是通过标签,我们可以添加这些功能。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将它放入了模板的字符串部分。JavaScript理应由JavaScript引擎进行处理,字符串部分由Jack的i18n标签进行处理。使用者可以通过i18n的文档了解到,:c(CAD)代表加拿大元的货币单位。 这就是标签模板的大部分实际应用了。 * 模板字符串不能代替Mustache和Nunjucks,一部分原因是在模板字符串没有内建的循环或条件语句语法。我们一起来看如何解决这个问题,如果JS不提供这个特性,我们就写一个标签来提供相应支持。 ~~~ // 基于纯粹虚构的模板语言 // ES6标签模板。 var libraryHtml = hashTemplate`-
#for book in ${myBooks}
- #{book.title} by #{book.author} #end