字符串可以说是编程中最重要的数据类型。他们几乎存在每个高级编程语言中,高效地使用字符串是开发者创建有用的程序的基础。通过扩展,正则表达式十分重要,因为正则表达式给开发者提供的额外的功能操作字符串。考虑到这些事实,ECMAScript 6的创建者通过增加新功能和长期确实的功能,改善字符串类型和正则表达式。这章将介绍这两种类型的变化。
在ECMAScript 6之前,JavaScript字符串是16位字符编码(UTF-16)。每16位序列为一个字符串的代码单元。所有的字符串属性和方法,比如length
属性和charAt()
,都是基于这些16位的代码单元。当然,使用16位字符编码足以包含任何字符。由于Unicode引进扩展字符,16位字符编码不再满足需求。
限制字符的长度为16位不可能满足Unicode的目标,既给全世界的每一个字符提供唯一的编号。这些唯一的编号,被称为码点 ,他们都是从0开始的简单数字。码点是你们认为的字符代码,其中一个数标识一个字符。字符串编码必须将代码点编码为内部一致的代码单元。对于UTF-16,码点可以组成许多代码单元。
UTF-16中的前2^16个码点代表单个的16位代码单元。这区间称为基本平面 (BMP)。之外的字符都放在辅助平面 ,辅助平面 的码点不只是16位。UTF-16为了解决这个问题引入*“代理对”* ,单码点用两个16位代码单元表示。这就意味着字符串中任何单字符,要么是基本平面字符集中一个代码单元,总共16位,要么是辅助平面字符集的的两个单元,总共32位。
在ECMAScript 5中,所有的字符操作基于16位代码单元工作,意味着在获取包含“代理对”的UTF-16编码字符串时,你可能得到出乎意料的结果,比如这个例子:
var text = "𠮷";
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(text.charAt(0)); // ""
console.log(text.charAt(1)); // ""
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
单个Unicode字符“𠮷”
使用代理对,因此,JavaScript字符操作在处理这个字符串时,将把它视为两个16位的字符。这表示:
text
的长度为2,即使它应该是1。- 正则表达式不能匹配单字符,因为它会认为有两个字符。
charAt()
不能返回有效的字符串,两个16位字符都不符合可打印的字符串
charCodeAt()
方法也不能识别这个字符。它能对每个代码单元返回准确的16位数,但这是你在ECMAScript 5中最有可能得到text
的真实值的方法。
另一方面,ECMAScript 6强制UTF-16字符串编码来解决这些问题。标准的字符串操作基于这种字符编码,这表示JavaScript可以支持专用于“代理对”的功能。本节的其余部分将讨论该功能的几个关键示例。
codePointAt()方法是ECMAScript 6为了完全支持UTF-16新添加的一个方法,它通过给定的一个字符串中的位置去匹配,取出Unicode码点。这个方法接收的是代码单元的位置,而不是字符的位置,并且返回一个整数值,比如示例中的这些console.log()
:
var text = "𠮷a";
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
console.log(text.charCodeAt(2)); // 97
console.log(text.codePointAt(0)); // 134071
console.log(text.codePointAt(1)); // 57271
console.log(text.codePointAt(2)); // 97
除了non-BMP的字符集,codePointAt()
方法和charCodeAt()
方法返回值相同,text
中的第一个字符是non-BMP字符,所以它是由两个代码单元组成,表示length
属性的值为3,而不是2。charCodeAt()
方法只返回位置为0的代码单元,但是codePointAt()返回完整的码点,即使码点跨越多个代码单元。两个方法都对位置1(第一个字符的第二个代码单元)和位置2(“a”
字符)返回了相同的值。
在字符上调用codePointAt()
是检验字符是否由一个或者两个码点表示的最简便方法。你可以写这个函数去检验:
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false
16位字符的上限用十六进制表示为FFFF
,所以任何在FFFF之上的码点必须由两个代码单元表示,总共32位。
当ECMASCript提供某种方式做某件事时,它也会提供一种方式做相反的事情。你能够使用codePointAt()
方法提取字符串中某个字符的码点,同时,String.fromCodePoint()
可以通过提供的码点产生一个单字符的字符串。例如:
console.log(String.fromCodePoint(134071)); // "𠮷"
String.fromCodePoint()
是String.fromCharCode()
方法更加完整的版本。这两个方法对所有在BMP中的字符中是一样的,只有当传入非BMP中字符的码点,才会有差别。
Unicode另外一个有趣的方面是,为了满足排序或其他基于比较的操作的目的,不同的字符被认为是相等的。有两种方式去定义这些关系。第一种,* 典型等价* 表示两个码点序列在所有方面是可互换的。例如,两个字符的组合可以规范地等价为一个字符。第二种关系是兼容等价。两个码点的兼容序列看起来不同,但是在某些场景是可互换的。
由于这些关系,基本上表示相同文本的两个字符串可以包含不同的码点序列。比如,字符"æ"和两个字符串"ae"可以换使用,但是严格意义上不同,除非用某种标准化。
给字符串提供了ECMAScript
方法,ECMAScript 6支持Unicode标准形式。这个方法可选择性接受一个单一字符串参数,表示使用下面某一种Unicode标准形式:
- 规范形式典型组合("NFC"),默认使用
- 规范形式典型分解("NFD")
- 规范形式兼容组合("NFKC")
- 规范形式兼容分解("NFKD")
解释四种形式的差别已经超出了本书的范围。只要记住,当比较字符串时,两个字符串必须标准化为同一种形式。比如:
var normalized = values.map(function(text) {
return text.normalize();
});
normalized.sort(function(first, second) {
if (first < second) {
return -1;
} else if (first === second) {
return 0;
} else {
return 1;
}
});
这段代码把values数组中的字符串转化为标准形式,所以,这个数组可以正确排序。你可以通过把调用normalize()
放在比较函数中,然后排序原始数组,如下:
values.sort(function(first, second) {
var firstNormalized = first.normalize(),
secondNormalized = second.normalize();
if (firstNormalized < secondNormalized) {
return -1;
} else if (firstNormalized === secondNormalized) {
return 0;
} else {
return 1;
}
});
同样,注意这段代码最重要的部分是,first
和second
规范化为同一种形式。这些例子使用了默认的NFC,但是你可以很容易指定其他规范形式,比如:
values.sort(function(first, second) {
var firstNormalized = first.normalize("NFD"),
secondNormalized = second.normalize("NFD");
if (firstNormalized < secondNormalized) {
return -1;
} else if (firstNormalized === secondNormalized) {
return 0;
} else {
return 1;
}
});
如果你之前从来没有考虑过Unicode规范,现在你可能不大使用这个方法。但是如果你曾经开发过国际应用,你肯定会发现normalize()
方法十分有用。
ECMAScript 6在支持Unicode字符串方面,这些方法不是唯一改进,标准规范还添加了两个有用的语法元素。
你可以使用正则表达式完成许多常用的字符串操作。但是记住,正则表达式是假设16位编码单元,每个编码单元代表一个单字符。为了解决这个问题,ECMAScript 6为正则表达式定义了一个u
修饰符,代表Unicode。
当正则表达式设置u修饰符后,它切换模式处理字符串,不是编码单元。这意味着,正则表达式不在对“代理对”感到迷惑,并且表现如期的行为。比如,看这段代码:
var text = "𠮷";
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(/^.$/u.test(text)); // true
正则表达式/^.$/
匹配任何输入的单字符组成的字符串。没有使用u
修饰符时,这个正则表达式匹配编码单元,所以这个日文字符(它是用两个编码单元表示)不会匹配这个正则表达式。当使用u
修饰符时,正则表达式比较字符串,而不是编码单元,所以这个日文字符可以匹配。
不幸的是,ECMAScript 6没有添加一个方法,去计算一个字符串有多少码点,但是利用u
标记,你可以使用正则表达式计算字符串的码点数,如下:
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
console.log(codePointLength("abc")); // 3
console.log(codePointLength("𠮷bc")); // 3
这个例子使用全局使用Uncode的正则表达式,调用match()
去检测text
中的空格或非空格字符(使用[\s\S]
确保模式匹配换行符)。只要有一个匹配,result
为一个匹配值的数组,所以数组的长度就是字符串中码点的数目。在字符串中"abc"
和"𠮷bc"
都有三个字符,所以这个数组长度都是3。
尽管这个方法生效,但是它效率不高,尤其是运用在长字符串上。你也可以使用一个字符串迭代器(在第八章讨论)。一般情况下,无论什么时候,尽量少去计算码点。
因为u
修饰符属于语法变化,在不兼容ECMAScript 6的JavaScript引擎中使用该语法,会报语法错误。检查是否支持u
修饰符最安全的方式是使用函数,比如这个:
function hasRegExpU() {
try {
var pattern = new RegExp(".", "u");
return true;
} catch (ex) {
return false;
}
}
这个函数调用RegExp
构造器,将u
修饰符作为参数传入。这种语法在以前的JavaScript引擎中也是有效的,如果不支持u
修饰符,这个构造器将会抛异常。
如果你代码需要在老一代的JavaScript引擎上工作,当使用u
修饰符时,一定要使用RegExp
构造器。这将防止语法错误,允许你在不中止程序执行的情况下,选择性的检查和使用u
修饰符。
JavaScript字符串总是落后于其他语言在字符串上类似的功能。在ECMAScript 5规范中,字符串才有trim()
方法,比如,ECMAScript 6任然在添加新功能去扩展JavaScript解析字符串的能力。
开发者已经在使用index()
方法检索在其他字符串中的字符串,因为它是JavaScript引入的第一个字符串检索方法。ECMAScript 6包括了如下三个方法,这些方法设计用于这些场景:
- 调用
includes()
时,如果在字符串的任何地方中检索到所指定的文本,该方法方法返回true,否则返回false。 - 调用
startsWith()
时,如果指定文本是字符串的开头,该方法返回true,否则返回false。 - 调用
endsWith()
时,如果指定文本是字符串的末尾,该方法返回true,否则返回false。
每个方法接收两个参数:要搜索的文本和可选的索引,索引用来指定在开始搜索的位置。当提供第二个参数时,includes()
和startsWith()
方法从字符串那个索引的位置开始匹配,endsWith()
方法从字符串长度减去第二个参数的地方开始匹配。当第二个参数省略时,includes()
和startsWith()
方法从字符串的开头开始匹配,endsWith()
方法则从字符串的末尾开始匹配。实际上,第二个参数最小化被搜索的字符串的数量。这里有一些例子,展示这三个方法的实际运用:
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
console.log(msg.endsWith("!")); // true
console.log(msg.includes("o")); // true
console.log(msg.startsWith("o")); // false
console.log(msg.endsWith("world!")); // true
console.log(msg.includes("x")); // false
console.log(msg.startsWith("o", 4)); // true
console.log(msg.endsWith("o", 8)); // true
console.log(msg.includes("o", 8)); // false
前三个方法调用没有传入第二个参数,所以它们可能搜索整个字符串。最后三个方法调用只检查部分字符串。msg.startsWith("o", 4)
调用从msg
字符串索引为4的位置开始匹配,也就是“Hello”中的“o”。msg.endsWith("o", 8)
调用也是从索引为4的位置开始匹配,因为字符串的长度(12)要减去第二个参数8。msg.includes("o", 8)
调用是从索引为8的位置开始匹配,也就“world”中的“r”位置。
虽然这三个方法很容易检索去字符串的存在性,但是它们直返一个布尔值。如果你需要找出一个字符串在另外一个字符串中实际位置,需要使用indexOf()
或者lastIndexOf()
方法。
如果给startsWith()
,endsWith()
,includes()
方法传入正则表达式而不是字符串,它们会报错。这indexOf()
和lastIndexOf()
方法相反,它们会把正则表达式参数转化为字符串,然后搜索该字符串。
ECMAScript 6 也给字符串添加了repeat()
方法,它接收一个参数,这个参数用于指定复制字符串的次数。该方法返回一个新字符串,包含原始字符串指定次数的复制。比如:
console.log("x".repeat(3)); // "xxx"
console.log("hello".repeat(2)); // "hellohello"
console.log("abc".repeat(4)); // "abcabcabcabc"
首先这个方法很方便,当你操作文本时很有用。尤其是代码格式化程序中,需要创建缩进等级,比如:
// 指定缩进的空格数量
var indent = " ".repeat(4),
indentLevel = 0;
// 当你增加缩进时
var newIndent = indent.repeat(++indentLevel);
第一个repeat()
调用一个包含四个空格的字符串,indentLevel
变量用来追踪缩进等级。然后,你可以传入递增的indentLevel
调用repeat()
方法,来改变空格数。
ECMAScript 6也对不适合特定场合的正则表达式的功能做一些有用的改动,下一节重点介绍几个。
正则表达式是JavaScript中处理字符串重要的一部分,和该编程语言的其他部分一样,在最近的版本中没有太多变化。然而,ECMAScript 6在升级字符串的同时,也对正则表达式做了一些改善。
火狐浏览器实现y
修饰,作为正则表达式的专有扩展,之后ECMAScript 6也把y
作为了一种标准。y
修饰符影响正则表达式搜索的sticky
属性,它指示搜索在正则表达式的lastIndex属性指定的位置开始匹配字符串的字符。如果在那个位置没有匹配上,正则表达式就会停止匹配。要了解它是如何工作的,请看如下代码:
var text = "hello1 hello2 hello3",
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "
pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // Error! stickyResult is null
这个例子有三个正则表达式,pattern
中的表达式没有修饰符,globalPattern
中的表达式使用了g
修饰符,stickyPattern
中的表达式使用了y
修饰符。在前三个console.log()调用中,三个正则表达式都返回了"hello1 "
,该字符串末尾带一个空格。
之后,三个正则表达式的lastIndex
属性都设置为1,这表示着,正则表达式应该从字符串的第二字符开始匹配。没有修饰符的正则表达式完全忽视lastIndex
属性的改变,仍然不出意外匹配到"hello1 "
。使用g
修饰符的正则表达式继续匹配到"hello2 ",因为它是从字符串的第二个字符“e”往前搜索。粘性正则表达式从第二字符串开始搜索,没有匹配到任何字符串,所以“stickyResult”依旧为null。
每次执行操作室,粘性修饰符保存了lastIndex
最后一次匹配的下一个字符的索引。如果操作的结果没有匹配上,lastIndex
就会设置为0,全局修饰符也是同样的行为,如这里所示:
var text = "hello1 hello2 hello3",
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 7
console.log(stickyPattern.lastIndex); // 7
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // "hello2 "
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 14
console.log(stickyPattern.lastIndex); // 14
对于stickyPattern和globalPattern变量,在第一次执行exec()之后,lastIndex的值变为7,第二次调用后变为14。有两个关于粘性修饰符的细节需要记住:
- 只有在调用正则表达式对象上的方法,才能使用lastIndex属性,比如exec()和test()方法,把正则表达式传给字符串的方法,比如match(),这不会导致粘性行为。
- 当使用^字符去匹配字符串的开头时,粘性正则表达式只会从字符串的开始匹配(或者是多行模式下,每行的开始)。当lastIndex的属性为0时,^使得粘性正则表达式和非粘性正则表达式没区别。如果在单行模式下,lastIndex属性不是对应字符串的开头,或者在多行模式下,不是对应一行的开头,粘性正则表达式将不会生效。
和其他正则表达式一样,你可以使用属性检测y修饰符的存在性。在这个例子中,你可以检测sticky属性,如下:
var pattern = /hello\d/y;
console.log(pattern.sticky); // true
如果存在粘性修饰符,sticky属性为true,否则为false。sticky属性是基于y修饰符存在的只读属性,不能再代码里修改。
和u修饰符一样,y修饰符是语法变化,在老的JavaScript引擎中,它可能造成语法错误。你可以使用如下方法去检查是否支持:
function hasRegExpY() {
try {
var pattern = new RegExp(".", "y");
return true;
} catch (ex) {
return false;
}
}
就和u
修饰符检验一样,如果不能创建一个带y修饰符的正则表达式,就返回false。最后一个和u修饰符相似之处,如果你需要在旧的JavaScript引擎中使用y修饰符,确保在定义这些正则表达式时,使用RegExp构造器去避免语法错误。
在ECMAScript 5中,你可以复制正则表达式,通过把他们传递到RegExp构造器中,比如:
var re1 = /ab/i,
re2 = new RegExp(re1);
re2变量只是re1变量的副本,但是如果你给RegExp构造器提供第二个参数,给正则表达式声明修饰函数,你的代码就不会生效了,比如:
var re1 = /ab/i,
// 在ES5中会报错,在ES6中不会
re2 = new RegExp(re1, "g");
如果你在ECMAScript 5环境运行这段代码,你会收到一个错误,这表明当地一个参数是正则表达式式时,不能传入第二个参数。ECMAScript 6改变了这种行为,所以第二个参数是允许的,可以覆盖第一个参数中的任何修饰符。比如:
var re1 = /ab/i,
// 在ES5中会报错,ES6中不会
re2 = new RegExp(re1, "g");
console.log(re1.toString()); // "/ab/i"
console.log(re2.toString()); // "/ab/g"
console.log(re1.test("ab")); // true
console.log(re2.test("ab")); // true
console.log(re1.test("AB")); // true
console.log(re2.test("AB")); // false
在这段代码中,res1有不区分大小写的i修饰符,同时re2只有全局g修饰符,RegExp构造器从re1复制模式,用g修饰符替代i修饰符。如果没有第二个参数,re2和re1的修饰符将会一样。
随着添加新的修饰符,改变修饰符工作的方式,ECMAScript 6添加了一个关联他们的属性。在ECMAScript 5中,你可以使用source属性得到正则表达式的文本,但是为了得到修饰符字符串,你必须解析toString()方法的输出,如下:
function getFlags(re) {
var text = re.toString();
return text.substring(text.lastIndexOf("/") + 1, text.length);
}
// toString() is "/ab/g"
var re = /ab/g;
console.log(getFlags(re)); // "g"
这段代码把正则表达式转换为了字符串,然后返回在最后一个/之后的字符,这些都是修饰符。
ECMAScript 6通过添加flags属性,使得获取修饰符更加容易,flags属性和source属性是一起加入ECMAScript 6的。两个属性都是原型访问属性,只分配一个getter,所以为只读属性。flags属性在debug和继承时,让检查正则表达式变得更加容易。
在晚期ECMAScript 6的增订版本中,flags属性返回正则表达式中的所有修饰符的字符串表现形式。例如:
var re = /ab/g;
console.log(re.source); // "ab"
console.log(re.flags); // "g"
这个例子获取了re表达式中所有的修饰符,并且比使用toString()方法更少的代码,把修饰符的字符串打印到控制台。一起使用source和flags属性,允许你抽取出正则表达式的一些片段,而不是直接解析正则表达式字符串。
到现在为止,这章所覆盖的字符串和正则表达式的更改是绝对强大的,但是ECMAScript 6以一种更强大的方式提高你对字符串的控制能力,它给表格添加了一种字面量,这使得字符串更加灵活。
JavaScript的字符串对比其他语言的字符串,总是只具有相对有限的功能。比如,直到ECMAScript 6,字符串一直缺少上一节所涉及的方法,而且字符串拼接功能也是很简单。为了允许开发者解决更多复杂的问题,ECMAScript 6的模板字面量提供了创建特定域语言(DSLs)的语法,相对ECMAScript 5和之前的版本,这是一种使操作内容更加安全的方式。(DSL是一种为具体和狭小的目的而设计的语言,和一般意义上的语言相反,比如JavaScript。)ECMAScript对模板字面量稻草人给出了如下描述:
这个方案通过使用语法糖扩展了ECMAScript的语法,允许库提供DSLs,可以容易的创建,查询和操作来自其他语言的内容,这些操作可以抵抗注入攻击,比如XSS,SQL注入等等。
事实上,模板字面量是ECMAScript 6对于如下特性的回答,所有这些特性都是ECMAScript 5所有缺少的。
- 多行字符串 :多行字符串正式的概念
- 基本字符串格式:将字符串的部分内容替换为变量的值的能力。
- HTML转义:转换字符换的能力,使得能够安全地插入HTML。
不是对已经存在的JavaScript字符串添加更多的功能,模板字面量是一个解决以上问题的全新方案。
最简单的模板字面量就像是被反引号(`)分割的普通的字符串,而不是双引号或者单引号。比如,看下如下代码:
let message = `Hello world!`;
console.log(message); // "Hello world!"
console.log(typeof message); // "string"
console.log(message.length); // 12
这段代码演示message
变量包含一个普通的JavaScript字符串。模板字面量语法用来创建一个字符串值,然后把它赋值给message
变量。
如果你想在字符串中使用反引号,你只需要用反斜杠去转义它,比如在这个版本的message变量中:
let message = `\`Hello\` world!`;
console.log(message); // "`Hello` world!"
console.log(typeof message); // "string"
console.log(message.length); // 14
在模板字面量中没有必要转义双引号或者单引号。
自从JavaScript的第一个版本以来,JavaScript开发者就已经想用一种方式创建多行字符串。但是当使用多行或者单行字符串时,字符串必须完全在一行。
多亏长期的一个语法bug,JavaScript确实有一个解决方案。如果在新的一行之前有一个反斜杠(\),你可以创建多行字符串。这里有一个例子:
var message = "Multiline \
string";
console.log(message); // "Multiline string"
message字符串在控制台打印时,没有出现换行,因为反斜杠被认为是一个连续符,而不是新的一行。为了在输出中换行,我们需要手动加入换行符:
var message = "Multiline \n\
string";
console.log(message); // "Multiline
// string"
在所有主要的JavaScript引擎中,这段代码都会把“Multiline String”分两行打印,但是这个行为被定为一个bug,许多开发者都不推荐使用。
其他在ES6之前创建多行字符串的解决方案,通常都依赖数组或字符串拼接,比如:
var message = [
"Multiline ",
"string"
].join("\n");
let message = "Multiline \n" +
"string";
开发者对JavaScript多行字符串缺失问题,使用的所有解决方案都有待改进。
ECMAScript 6的模板字面量让多行字符串变得十分容易,因为不需要特殊的语法。你可以在任意想要的地方换行,就会在结果里出现。比如:
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()方法移除初始化的空行。
在这点上,模板字面量可能看起来像普通JavaScript字符串的fancier版本。两者之间的真正区别在于模板字面量的替换。替换允许你在一个模板字面量中,嵌入任何有效的JavaScript表达式,而且输出的结果为字符串的组成部分。
替换用${符号作为开始,用}作为闭合,里面可以包含任意JavaScript表达式。最简单的替换允许你直接在结果字符串中嵌入局部变量。比如:
let name = "Nicholas",
message = `Hello, ${name}.`;
console.log(message); // "Hello, Nicholas."
替换标志${name}会访问局部变量name,然后把name的值插入message字符串中。然后message变量就会立即保存“替换”的结果。
因为所有的替换都是JavaScript表达式,你可以替换的不仅仅是一个简单变量名。你可以很容易地嵌入计算,函数调用等等。比如:
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()格式化为两位小数。第二个替换之前的美元符号按照原形输出,因为它后面没有跟打开的大括号。
模板字面量也是JavaScript表达式,这意味着你可以在另外一个模板字面量中替换一个模板字面量。比如:
let name = "Nicholas",
message = `Hello, ${
`my name is ${ name }`
}.`;
console.log(message); // "Hello, my name is Nicholas."
这个例子在第一个模板字面量内部嵌入第二个模板字面量。在第一个 ${之后,另外一个模板字面量开始。第二个${表示在模板字面量内部的内嵌表达式的开始。那个表达式是一个name变量,它被插入到了结果中。
现在你已经看过了模板字面量是如何创建多行字符串的,而且不需要拼接就可以把值插入字符串。但是模板字面量真正的威力来自于标记模板。模板标记是对模板字面量的转换,并且返回最后的字符串值。这个标记在模板的开始指定,就是第一个`字符之前,如下所示:
let message = tag`Hello world`;
在这个例子中,tag是应用在Hello world
模板字面量中的模板标记。
tag只是一个函数,在处理模板字面量数据的时候调用。这个tag把获取模板字面量的数据作为私有片段,必须组合这些片段去生成结果。第一个参数是一个包含字符串的数组,由JavaScript解析得到。随后的每个参数是每个替换的解析值。
Tag函数通常是使用如下参数定义,是的处理数据更容易:
function tag(literals, ...substitutions) {
// return a string
}
为了更好地理传递给tags的值,看如下代码:
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]通常是字符串的结尾。替换总比字面量少一个,这意味着表达式substitutions.length === literals.length - 1总是对的。
使用这个模式,literals和substitutions数组可以交叉生成结果字符串。首先是literals的第一项,然后是substitutions的第一项等等,直到整个字符串完成。比如,你可以通过通过交替两个数组中的值,来模拟模板字面量的默认行为:
function passthru(literals, ...substitutions) {
let result = "";
// 只用替换的计数运行循环
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标签,展示了和默认模板字面量行为相同的转化。仅有的技巧是对循环体使用lengthsubstitutions.length,而不是literals.length,避免意外越过substitutions数组边界。这是生效的,因为在ECMAScript 6中对literals和substitutions的关系都有很好的定义。
substitutions包含的值不一定是字符串。如果一个表达式计算为一个数字,比如前面一个例子,然后数字的值就会传入。决定这些值在结果中应该怎样输出是标签函数的工作。
模板标签也可以访问原始字符串的内容,这就意味着,要在字符串转义符转换成等价含义之前,访问它们。操作原始字符串的最简单的方法是使用内置的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]等价,literals.raw[0]包含原始字符串的内容。知道这点,你就可以使用如下代码模拟String.row():
function raw(literals, ...substitutions) {
let result = "";
// run the loop only for the substitution count
for (let i = 0; i < substitutions.length; i++) {
result += literals.raw[i]; // use raw values instead
result += substitutions[i];
}
// add the last literal
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允许JavaScript在逻辑上可以处理UTF-16字符串。codePointAt()和String.fromCodePoit()的转换字符和码点的能力,对于字符串操作十分重要。添加正则表达式u修饰符使得操作码点成为可能,而不是操作16位的字符串,normalize()方法允许更规范的字符串比较。
ECMAScript 6也添加了一个新的方法,允许你能够更加容易地识别一个字符串,不管它在父字符串的什么位置。也有更多的功能添加到了正则表达式。
模板字面量对ECMAScript 6也是十分重要新增内容,它允许你创建特殊域语言(DSLs),使得创建字符串更加容易。直接在模板字面量中嵌入变量的能力,意味着开发者在使用变量组合长字符串时,拥有比字符串拼接更安全的工具。
内置支持多行字符串也使得模板字符串进行了一个有用的升级,普通字符串之前没有这个功能。尽管在模板字符串内直接允许多行字符串,你可以仍然可以使用'\n'和其他字符转义序列。
模板标签函数是创建DSLs特性最重要的部分。标签是接受模板字面量片段作为参数的函数。你可以使用那些数据返回一个合适的字符串值。这些数据包括字面量,他们的原始值和任何替换值。标签函数可以使用这些内容片段去决定正确的输出。