本文深入解析ES6引入的模板字符串特性,包括反引号语法、字符串插值、多行支持及转义规则。重点探讨标签模板机制,通过SaferHTML、i18n等示例展示如何利用标签函数处理特殊字符转义、国际化及自定义领域特定语言,揭示其作为函数调用的本质与强大灵活性。
模板字符串基础与字符串插值
ES6 引入了一种全新的字符串字面量形式——模板字符串。它使用反引号(`)进行标识,在外观上与普通字符串并无二致。在最基础的使用场景下,模板字符串的行为与普通字符串完全相同。
context.fillText(`Ceci n'est pas une cha?ne.`, x, y);
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}.`);
}
}
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` 的实际值分别插入到对应位置,最终生成如 “User jorendorff is not authorized to do hockey.” 这样的完整字符串。
这种语法相比传统的 `+` 运算符更加优雅,以下是模板字符串具备的一些关键特性:
模板占位符内部可以包含任意合法的 JavaScript 表达式,因此函数调用、四则运算等均为合法操作(甚至支持在模板字符串中嵌套另一个模板字符串)。若嵌入的值非字符串类型,JavaScript 会自动将其转换为字符串,例如若 `action` 为对象,则调用其 `.toString()` 方法进行转换。若需在模板字符串中使用反引号,需使用反斜杠(\)进行转义。同理,若需输出字面量的 `${`,也需使用反斜杠转义,即 `\${` 或 `$\{`。此外,模板字符串支持跨越多行书写:
$("#warning").html(`
<h1>Watch out!</h1>
<p>Unauthorized hockeying can result in penalties
of up to ${maxPenalty} minutes.</p>
`);
$("#warning").html(`
<h1>Watch out!</h1>
<p>Unauthorized hockeying can result in penalties
of up to ${maxPenalty} minutes.</p>
`);
在模板字符串中保留的所有空格、换行符及缩进格式,均会原样输出到最终生成的结果字符串中。
模板字符串的局限性
接下来,我们探讨模板字符串无法实现的功能:
模板字符串不会自动转义特殊字符,为防止跨站脚本(XSS)漏洞,处理不可信数据时仍需保持谨慎,这一点与普通字符串一致。它无法与国际化库直接配合使用,也不处理特殊语言格式的数字或日期。此外,它并非 Mustache 或 Nunjucks 等模板引擎的替代品,因为模板字符串缺乏处理循环的语法,无法直接通过数组构建表格(table)等复杂结构。
标签模板机制与实现
为了解决上述限制,ES6 为开发者和库设计者提供了另一种形式的模板字符串——标签模板。
标签模板的语法简洁,只需在起始反引号前添加一个标签标识符。参考第一个示例:`SaferHTML`,我们将利用此标签模板来解决前述的第一个限制,即自动转义特殊字符。
需要注意的是,`SaferHTML` 方法并非 ES6 标准库原生提供,需要开发者自行实现:
var message =
SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;
var message =
SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;
此处的 `SaferHTML` 标签仅为单个标识符,但标签也可以是属性访问形式,如 `SaferHTML.escape`,甚至是方法调用形式,如 `SaferHTML.escape({unicodeControlCharacters: false})`。准确地说,任何 ES6 的成员表达式或调用表达式均可作为标签使用。
由此可见,普通模板字符串仅是字符串连接的语法糖,而标签模板则是一个完全不同的概念:它本质上是一种函数调用。
因此,上述代码等价于以下函数调用形式:
var message =
SaferHTML(templateData, bonk.sender);
var message =
SaferHTML(templateData, bonk.sender);
其中 `templateData` 是一个不可变的字符串数组,由 JavaScript 引擎根据源模板字符串生成。由于模板字符串被占位符分隔后包含两个部分,因此 `templateData` 数组将包含两个元素,其结构如下:`Object.freeze(["<p>", " has sent you a bonk.</p>"])`。
(事实上,`templateData` 对象上还有另一个属性:`templateData.raw`。本文不深入探讨该属性。该属性的值也是一个数组,包含了标签模板中所有的字符串部分,但字符串中保留了转义序列,使其看起来更像源代码中的原始字符串,例如 `\n`。ES6 的内置标签 `String.raw` 将使用这些字符串。)
这使得 `SaferHTML` 方法可以自由解析这两个字符串,存在多种替换和处理方式。
在继续阅读之前,读者可能会思考如何实现 `SaferHTML` 方法。
以下是一种可能的实现方式(参考 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
借助上述方法,即使传入恶意的用户名,用户数据也是安全的。
标签模板的高级应用与灵活性
一个简单的示例尚不足以完全展示标签模板的灵活性,让我们回顾前文列举的模板字符串限制,看看标签模板还能做些什么。
虽然模板字符串本身不会自动转义特殊字符,但通过标签模板可以解决此问题,事实上我们还可以优化 `SaferHTML` 方法。从安全角度看,前述的 `SaferHTML` 实现较为脆弱。在 HTML 中,不同上下文需要不同的转义方式,而该实现并未涵盖所有情况。稍加思考,即可实现更灵活的 `SaferHTML` 方法,它能够识别 `templateData` 中每个占位符的上下文:是纯 HTML 内容、元素属性(需转义 `'` 和 `"`),还是 URL 查询字符串(需使用 URL 转义方法而非 HTML 转义)。虽然这看似有些牵强,因为 HTML 转义效率较低,但幸运的是,标签模板中的字符串是不可变的,`SaferHTML` 可以缓存已转义的字符串,从而提高效率。 模板字符串虽无内置国际化特性,但通过标签模板可添加该功能。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.
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)`,这将由 `i18n` 标签处理。根据 `i18n` 文档说明,`:c(CAD)` 表示 `amount` 为加拿大美元货币值。
模板字符串无法完全替代 Mustache 和 Nunjucks 等模板引擎,部分原因在于它不支持循环和条件语句。我们可以编写一个标签来实现这类功能:
// 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>
`;
// 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、JavaScript 数据结构、GL 着色器等。
标签模板允许库设计者创建强大的领域特定语言(DSL)。这些语言可能看起来不像 JavaScript,但它们可以无缝嵌入到 JavaScript 中,并与语言的其他部分交互。顺便提一下,我尚未在其他语言中见过类似特性,尽管尚不清楚这一特性将带来何种深远影响,但其展现出的各种可能性依然令人兴奋。