JS 的字符串相对其他语言来说功能总是有限的。为了让开发者能够解决复杂的问题, ES6
的模板字面量( template literal )提供了创建领域专用语言(
domain-specific language
,
DSL
)的语法,与 ES5 及更早版本的解决方案相比,处理内容可以更安全(领域专用语言是
被设计用于特定有限目的的编程语言,与通用目的语言如 JavaScript 相反)。 ECMAScript
wiki 在 template literal strawman 上提供了如下描述:
本方案通过语法糖扩展了 ECMAScript 的语法,允许语言库提供 DSL 以便制作、查询并
操纵来自于其它语言的内容,并且对注入攻击( 如 XSS 、 SQL 注入,等等 )能够免疫
或具有抗性。
不过实际上,模板字面量是 ES6 针对 JS 直到 ES5 依然完全缺失的如下功能的回应:
- 多行字符串:针对多行字符串的形式概念;
- 基本的字符串格式化:将字符串部分替换为已存在的变量值的能力;
- HTML 转义:能转换字符串以便将其安全插入到 HTML 中的能力。
- 模板字面量以一种新的方式解决了这些问题,而并未给 JS 已有的字符串添加额外功能。
基本语法
模板字面量的最简单语法,是使用反引号( ` )来包裹普通字符串,而不是用双引号或单
引号。参考以下例子:
let message = `Hello world!`;
console.log(message); // "Hello world!"
console.log(typeof message); // "string"
console.log(message.length); // 12
此代码说明了 message
变量包含的是一个普通的 JS 字符串。模板字面量语法被用于创建一
个字符串值,并被赋值给了 message
变量。
若你想在字符串中包含反引号,只需使用反斜杠( \ )转义即可,就像下面这个版本的
message
变量:
let message = `\`Hello\` world!`;
console.log(message); // "`Hello` world!"
console.log(typeof message); // "string"
console.log(message.length); // 14
在模板字面量中无需对双引号或单引号进行转义。
多行字符串
JS 开发者从该语言最初版本起就一直想要一种能创建多行字符串的方法。但在使用双引号或
单引号时,整个字符串只能放在单独一行。
ES6 之前的权宜之计
感谢存在已久的一个语法 bug , JS 的确有一种权宜之计:在换行之前的反斜线( \ )可以
用于创建多行字符串。这里有个范例:
var message = "Multiline \
string";
console.log(message); // "Multiline string"
message
字符串打印输出时不会有换行,因为反斜线被视为续延符号而不是新行的符号。为
了在输出中显示换行,你需要手动包含它:
var message = "Multiline \n\
string";
console.log(message); // "Multiline
// string"
在所有主流的 JS 引擎中,此代码都会输出两行,但是该行为被认定为一个 bug ,并且许多
开发者都建议应避免这么做。
其他 ES6 之前创建多行字符串的尝试,一般都基于数组或字符串的拼接,就像这样:
var message = [
"Multiline ",
"string"
].join("\n");
let message = "Multiline \n" +
"string";
关于 JS 缺失的多行字符串功能,开发者的所有解决方法都不够完美。
多行字符串的简单解决方法
ES6 的模板字面量使多行字符串更易创建,因为它不需要特殊的语法。只需在想要的位置包
含换行即可,而且它会显示在结果中。例如:
let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
反引号之内的所有空白符都是字符串的一部分,因此需要留意缩进。例如:
let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 31
此代码中,模板字面量第二行前面的所有空白符都被视为字符串自身的一部分。如果让多行
文本保持合适的缩进对你来说很重要,请考虑将多行模板字面量的第一行空置并在此后进行
缩进,如下所示:
let html = `
<div>
<h1>Title</h1>
</div>`.trim();
此代码从第一行开始创建模板字面量,但在第二行之前并没有包含任何文本。 HTML 标签的
缩进增强了可读性,之后再调用 trim() 方法移除了起始的空行。
如果你喜欢的话,也可以在模板字面量中使用 \n 来指示换行的插入位置:
let message = `Multiline\nstring`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
制造替换位
此时模板字面量看上去仅仅是普通 JS 字符串的升级版,但二者之间真正的区别在于前者
的“替换位”。替换位允许你将任何有效的 JS 表达式嵌入到模板字面量中,并将其结果输出为
字符串的一部分。
替换位由起始的 ${ 与结束的 }
来界定,之间允许放入任意的 JS 表达式。最简单的替换
位允许你将本地变量直接嵌入到结果字符串中,例如:
let name = "Nicholas",
message = `Hello, ${name}.`;
console.log(message); // "Hello, Nicholas."
替换位 ${name}
会访问本地变量 name
,并将其值插入到 message
字符串中。 message
变量会立即保留该替换位的结果。
模板字面量能访问到作用域中任意的可访问变量。试图使用未定义的变量会抛出错误,
无论是严格模式还是非严格模式。
既然替换位是 JS 表达式,那么可替换的就不仅仅是简单的变量名。你可以轻易嵌入计算、函
数调用,等等。例如:
let count = 10,
price = 0.25,
message = `${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."
此代码在模板字面量的一部分执行了一次计算, count 与 price 变量相乘,再使用
.toFixed() 方法将结果格式化为两位小数。而在第二个替换位之前的美元符号被照常输出,
因为没有左花括号紧随其后。
模板字面量本身也是 JS 表达式,意味着你可以将模板字面量嵌入到另一个模板字面量内部,
如同下例:
let name = "Nicholas",
message = `Hello, ${
`my name is ${ name }`
}.`;
console.log(message); // "Hello, my name is Nicholas."
模板字面量本身也是 JS 表达式,意味着你可以将模板字面量嵌入到另一个模板字面量内部,如同下例:
let name = "Nicholas",
message = `Hello, ${
`my name is ${ name }`
}.`;
console.log(message); // "Hello, my name is Nicholas."
此例在第一个模板字面量中套入了第二个。在首个 ${
之后使用了另一个模板字面量,第二
个 ${
标示了嵌入到内层模板字面量的表达式的开始,该表达式为被插入结果的 name 变
量。
标签化模板
现在你已了解模板字面量在无须连接的情况下,是如何创建多行字符串以及将值插入字符
串。不过模板字面量真正的力量来源于标签化模板。一个模板标签( template tag )
能对模
板字面量进行转换并返回最终的字符串值,标签在模板的起始处被指定,即在第一个 ` 之
前,如下所示:
let message = tag`Hello world`;
在本例中, tag 就是会被应用到 Hello world
模板字面量上的模板标签。
定义标签
一个标签( tag )仅是一个函数,它被调用时接收需要处理的模板字面量数据。标签所接收
的数据被划分为独立片段,并且必须将它们组合起来以创建结果。第一个参数是个数组,包
含被 JS 解释过的字面量字符串,随后的参数是每个替换位的解释值。
标签函数的参数一般定义为剩余参数形式,以便更容易处理数据,如下:
function tag(literals, ...substitutions) {
// 返回一个字符串
}
为了更好地理解传递给标签的是什么参数,可研究下例:
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
如果你拥有一个名为 passthru()
的函数,该函数将会接收到三个参数。首先是一个
literals
数组,包含如下元素:
- 在首个替换位之前的空字符串( “” );
- 首个替换位与第二个替换位之间的字符串( " items cost $" );
- 第二个替换位之后的字符串( “.” )。
接下来的参数会是 10
,也就是 count
变量的解释值,它也会成为 substitutions
数组的
第一个元素。最后一个参数则会是 "2.50"
,即 (count * price).toFixed(2)
的解释值,并
且会是 substitutions
数组的第二个元素。
需要注意 literals
的第一个元素是空字符串,以确保 literals[0]
总是字符串的起始部
分,正如 literals[literals.length - 1]
总是字符串的结尾部分。同时替换位的元素数量也
总是比字面量元素少 1 ,意味着表达式 substitutions.length === literals.length - 1
的值
总是 true 。
使用这种模式,可以交替使用 literals
与 substitutions
数组来创建一个结果字符串:以literals
中的首个元素开始,后面紧跟着 substitutions
中的首个元素,如此反复,直到结果字符串被创建完毕。你可以像下例这样交替使用两个数组中的值来模拟模板字面量的默认行为:
function passthru(literals, ...substitutions) {
let result = "";
// 仅使用 substitution 的元素数量来进行循环
for (let i = 0; i < substitutions.length; i++) {
result += literals[i];
result += substitutions[i];
}
// 添加最后一个字面量
result += literals[literals.length - 1];
return result;
}
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."
本例定义了 passthru
标签,能够执行与模板字面量的默认行为相同的转换操作。唯一的诀
窍是在循环中使用 substituions.length
而不是 literals.length
来避免 substituions
数
组的越界。它能工作是由于 ES6 对 literals
和 substituions
的良好定义。
substituions 中包含的值不必是字符串。若表达式的计算结果为数字(就像上例),那
么该数值也会被传入。决定这些值如何在结果中输出是标签的工作之一。
使用模板字面量中的原始值
模板标签也能访问字符串的原始信息,主要指的是可以访问字符在转义之前的形式。获取原
始字符串值的最简单方式是使用内置的 String.raw()
标签。例如:
let message1 = `Multiline\nstring`,
message2 = String.raw`Multiline\nstring`;
console.log(message1); // "Multiline
// string"
console.log(message2); // "Multiline\\nstring"
此代码中, message1
中的 \n
被解释为一个换行,而 message2
中的 \n
返回了它的原始
形式 "\\n"
(反斜线与 n
字符)。像这样提取原始字符串信息可以在必要时进行更复杂的
处理。
字符串的原始信息同样会被传递给模板标签。标签函数的第一个参数为包含额外属性 raw
的
数组,而 raw
属性则是含有与每个字面量值等价的原始值的数组。例如, literals[0]
的
值总是等价于包含字符串原始信息的 literals.raw[0]
的值。知道这些之后,你可以用如下
代码来模拟 String.raw()
:
function raw(literals, ...substitutions) {
let result = "";
// 仅使用 substitution 的元素数量来进行循环
for (let i = 0; i < substitutions.length; i++) {
result += literals.raw[i]; // 改为使用原始值
result += substitutions[i];
}
// 添加最后一个字面量
result += literals.raw[literals.length - 1];
return result;
}
let message = raw`Multiline\nstring`;
console.log(message); // "Multiline\\nstring"
console.log(message.length); // 17
这里使用 literals.raw
而非 literals
来输出结果字符串。这意味着任何转义字符(包括Unicode 代码点的转义)都会以原始的形式返回。当你想在输出的字符串中包含转义字符时,原始字符串会很有帮助(例如,若想要生成包含代码的文档,那么应当输出如表面看到那样的实际代码)。
总结
完整的 Unicode 支持允许 JS 以合理的方式处理 UTF-16 字符。通过 codePointAt()
与
String.fromCodePoint()
在代码点和字符之间转换的能力,是字符串操作的一大进步。正则
表达式新增的 u
标志使得直接操作代码点而不是 16 位字符变为可能,而 normalize()
方
法则允许进行更恰当的字符串比较。
ES6 也添加了操作字符串的新方法,允许你更容易识别子字符串,而不用管它在父字符串中
的位置。正则表达式同样引入了许多功能。
模板字面量是 ES6 的一项重要补充,允许你创建领域专用语言( DSL )让字符串的创建更容
易。能将变量直接嵌入到模板字面量中,意味着开发者在组合长字符串与变量时,有了一种
比字符串拼接更为安全的工具。
内置的多行字符串支持,是普通 JS 字符串绝对无法做到的,这使得模板字面量成为凌驾于前
者之上的有用升级。尽管在模板字面量中允许直接使用换行,你依然可以使用 \n 或其它字
符转义序列。
模板标签是创建 DSL 最重要的部分。标签是接收模板字面量片段作为参数的函数,你可以使
用它们来返回合适的字符串。这些数据包括了字面量、等价的原始值以及替换位的值,标签
使用这些信息片段来决定输出。