Skip to content

Latest commit

 

History

History
697 lines (482 loc) · 36.3 KB

02-字符串和正则表达式.md

File metadata and controls

697 lines (482 loc) · 36.3 KB

字符串和正则表达式

字符串可以说是编程中最重要的数据类型。他们几乎存在每个高级编程语言中,高效地使用字符串是开发者创建有用的程序的基础。通过扩展,正则表达式十分重要,因为正则表达式给开发者提供的额外的功能操作字符串。考虑到这些事实,ECMAScript 6的创建者通过增加新功能和长期确实的功能,改善字符串类型和正则表达式。这章将介绍这两种类型的变化。

更好的Unicode支持

在ECMAScript 6之前,JavaScript字符串是16位字符编码(UTF-16)。每16位序列为一个字符串的代码单元。所有的字符串属性和方法,比如length属性和charAt(),都是基于这些16位的代码单元。当然,使用16位字符编码足以包含任何字符。由于Unicode引进扩展字符,16位字符编码不再满足需求。

UTF-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()方法

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位。

String.fromCodePoint()方法

当ECMASCript提供某种方式做某件事时,它也会提供一种方式做相反的事情。你能够使用codePointAt()方法提取字符串中某个字符的码点,同时,String.fromCodePoint()可以通过提供的码点产生一个单字符的字符串。例如:

console.log(String.fromCodePoint(134071));  // "𠮷"

String.fromCodePoint()String.fromCharCode()方法更加完整的版本。这两个方法对所有在BMP中的字符中是一样的,只有当传入非BMP中字符的码点,才会有差别。

normalize()方法

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;
    }
});

同样,注意这段代码最重要的部分是,firstsecond规范化为同一种形式。这些例子使用了默认的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字符串方面,这些方法不是唯一改进,标准规范还添加了两个有用的语法元素。

正则表达式u修饰符

你可以使用正则表达式完成许多常用的字符串操作。但是记住,正则表达式是假设16位编码单元,每个编码单元代表一个单字符。为了解决这个问题,ECMAScript 6为正则表达式定义了一个u修饰符,代表Unicode。

u标记实战

当正则表达式设置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修饰符

因为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()方法相反,它们会把正则表达式参数转化为字符串,然后搜索该字符串。

repeat()方法

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修饰符

火狐浏览器实现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。有两个关于粘性修饰符的细节需要记住:

  1. 只有在调用正则表达式对象上的方法,才能使用lastIndex属性,比如exec()和test()方法,把正则表达式传给字符串的方法,比如match(),这不会导致粘性行为。
  2. 当使用^字符去匹配字符串的开头时,粘性正则表达式只会从字符串的开始匹配(或者是多行模式下,每行的开始)。当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的修饰符将会一样。

flags属性

随着添加新的修饰符,改变修饰符工作的方式,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开发者就已经想用一种方式创建多行字符串。但是当使用多行或者单行字符串时,字符串必须完全在一行。

ECMAScript 6之前的解决方案

多亏长期的一个语法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特性最重要的部分。标签是接受模板字面量片段作为参数的函数。你可以使用那些数据返回一个合适的字符串值。这些数据包括字面量,他们的原始值和任何替换值。标签函数可以使用这些内容片段去决定正确的输出。