撇号基础知识(`)
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
插入到结果字符串中。
到目前为止,这只是+
操作符的一个稍微更好的语法,下面的细节是你所期望的:
- 模板替换中的代码可以是任何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),您仍然必须小心处理不可信的数据,就像连接普通字符串一样。
- 模板字符串不处理数字和日期的特定语言格式,更不用说复数格式了。
- 它们不是模板库的替代品,就像
Mustache
或Nunjucks
- 模板字符串没有任何用于循环的内置语法——例如,从数组中构建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
是另一个数组,包含了带标签的模板中的所有字符串部分,但这一次与它们在源代码中看到的完全一样——像
这样的转义序列保持完整,而不是被转换成换行符等等。标准标记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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// 不转义模板字符串的其他部分(除了插值)
s += templateData[i];
}
return s;
}
单个示例不足以说明标记模板的灵活性。让我们回顾一下之前的模板字符串限制列表,看看还可以做些什么:
- 模板字符串不会自动转义特殊字符。但是正如我们所看到的,有了标记模板,您可以用标记自己解决这个问题。事实上,你可以做得更好。从安全角度来看,我的SaferHTML函数非常弱。HTML中的不同位置有不同的特殊字符,需要以不同的方式转义;SaferHTML并没有对它们全部进行转义。但是通过一些努力,你可以编写一个更智能的SaferHTML函数,它实际解析templateData字符串中的HTML位置,这样它就知道哪些替换是在普通HTML中;哪些是元素属性内部的,因此需要转义
'
和"
;哪些是在URL查询字符串中,因此需要URL转义而不是html转义;等等。它可以对每个需要替换的字符串执行正确的转义。这听起来是不是有些牵强,因为HTML解析很慢?幸运的是,当再次计算模板时,已标记模板的字符串部分不会更改。SaferHTML可以缓存所有这些解析的结果,以加快以后的调用。 - 模板字符串没有内置的国际化特性。但是有了标签,我们可以添加它们。注意,在下面的这个例子中,
name
和amount
是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.
- 模板字符串不能替代
Mustache
和Nunjucks
,部分原因是它们没有用于循环或条件句的内置语法。但现在我们开始考虑如何解决这个问题了,对吧?如果JS不提供该特性,那么编写一个提供该特性的标记。
// 纯粹假设的模板语言(基于ES6的标记模板)
var libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
灵活性并不止于此。注意,标签函数的参数不会自动转换为字符串。它们可以是任何东西。返回值也是如此。带标签的模板甚至不一定是字符串!你可以使用自定义标签来创建正则表达式、DOM树、图像、代表整个异步进程的承诺、JS数据结构、GL着色器……