在介绍字符串之前,有必要先了解一点Unicode的基础知识,有助于理解ES6提供的新功能和新特性。
一、Unicode
Unicode是一种字符集(即多个字符的集合),它的目标是涵盖世界上的所有字符,为其提供唯一的标识符,这个标识符叫做码位或码点(Code Point)。码位既可以用一个从0开始计算的数值表示,也可以用U+作为前缀后面紧跟十六进制数表示。
Unicode只规定了每个字符的码位,但并没有规定如何用字节序列(即二进制数字存储方式)表示字符,于是就出现了字符编码(Character Encoding)。Unicode包含多种字符编码,例如UTF-8、UTF-16等,此处的UTF前缀是Unicode Transformation Format的缩写,即统一转换格式,它们都是Unicode的一种实现方式。其中UTF-8是变长编码,使用1~4个字节表示一个字符,它的最小编码单元(Code Unit)为一个字节(即8位);而UTF-16使用2或4个字节表示一个字符,它的最小编码单元为两个字节(即16位)。
Unicode的码位范围从U+0000到U+10FFFF,由于包含的字符众多,因此会把它们划分成17组,组也叫平面(Plane),每个平面包含2^16=65536个字符,其中第0个平面叫做基本多语言平面(Basic Multilingual Plane,简称BMP),码位范围从U+0000到U+FFFF(包含了ASCII码),剩下的16个为辅助平面(Supplementary Plane)。
JavaScript采用了UTF-16编码的Unicode字符集,BMP中的字符可用一个16位的编码单元表示,而辅助平面中的字符则要遵循UTF-16的代理对(Surrogate Pair)规则,即用两个编码单元表示。这意味着JavaScript中的一个Unicode字符,它的长度有可能是1,但也有可能是2。由于JavaScript中的字符串方法(例如substring()、charAt()等)都会受到这种编码规则的影响,因此有时候会返回出人意料的结果。不过好在ES6大幅增强了对Unicode的支持,有效避免了这种意外性情况的发生。
二、Unicode字符
在JavaScript中,Unicode字符可以用Unicode转义字符的形式(即\uXXXX)表示,其中4个“X”表示字符的码位,而“X”是一个16进制字符,还要注意一点,ES5只支持4个“X”。也就是说,这种形式只能表示BMP中的字符(即U+0000到U+FFFF内的字符),如果要使用辅助平面中的字符,那么需要写两个Unicode转义字符。下面代码中,第一个字符是BMP中的“向”,第二个字符是2号平面中的“𠮳”。
let word1 = "\u5411"; console.log(word1); //"向" let word2 = "\ud842\udfb3"; console.log(word2); //"𠮳"
ES6为Unicode字符提供了一种新形式,只需把码位用花括号包裹,就能支持辅助平面中的字符。下面使用了新形式来描述字符“𠮳”。
let word3 = "\u{20BB3}"; console.log(word3); //"𠮳"
三、Unicode标准化
Unicode标准化(Unicode Normalization),也叫Unicode正规化或Unicode规范化,可将字符转换成指定的字节序列,统一表现形式,以及确定字符之间的等价性。例如字符“ü”,既可以只用U+00FC表示,也可以用U+0075(u)和U+0308(¨)组合表示,虽然对于人类来说,两种表示法得到的结果在视觉上是完全相同的,但对于计算机来说却是不同的,如下所示。
var mark1 = "\u00FC", mark2 = "\u0075\u0308"; mark1 === mark2; //false
ES6新增了一个原型方法normalize(),可以将字符串标准化,修改上面的例子,就能得到相等的结果,如下所示。
mark1.normalize() === mark2.normalize(); //true
normalize()方法可以接收一个字符串参数,但只有4个可选值(如表4所示),其中“NFC”是方法的默认值。
表4 标准化参数
可选值 | 作用描述 |
NFD | 标准等价分解 |
NFC | 先以标准等价分解,再以标准等价合成 |
NFKD | 兼容等价分解 |
NFKC | 先以兼容等价分解,再以标准等价合成 |
上表中的标准等价(Canonical Equivalence)和兼容等价(Compatibility Equivalence)都表示相同的字符或字符序列,并且前者是后者的一个子集。标准等价会保持视觉外观和文本含义,前面字符“ü”的示例就用到了标准等价;而兼容等价会改变视觉外观和文本含义,例如罗马数字十二(Ⅻ)可由一个罗马数十(Ⅹ)和两个罗马数一(Ⅰ)组成,两者只有通过兼容等价的标准化处理后才能匹配成功,如下所示。
var digit1 = "\u216B", //"Ⅻ" digit2 = "\u2169\u2160\u2160"; //"ⅩⅠⅠ" digit1 = digit1.normalize("NFKC"); //"XII" digit2 = digit2.normalize("NFKC"); //"XII" digit1 === digit2; //true
四、码位的处理
字符串的原型方法charCodeAt()可以读取到BMP中的字符的码位,而辅助平面中的字符却无法正确读取,它们会被当成两个字符来对待。还是以“𠮳”为例,如下所示,分别返回字符串第0和第1处位置的码位。
var str = "𠮳"; str.charCodeAt(0); //55362 str.charCodeAt(1); //57267
ES6提供了codePointAt()方法,有效解决了上述问题,如下所示。
str.codePointAt(0); //134067 str.codePointAt(1); //57267
不过需要注意,codePointAt()方法还能返回字符的第二个编码单元的码位,即上面代码中第2条语句。
String对象的静态方法fromCharCode()可将码位转换成字符,功能和charCodeAt()方法正好相反,但也不能正确处理辅助平面中的字符。为此,ES6扩展了String对象,新增了一个静态方法fromCodePoint(),和codePointAt()方法对应,如下所示,由于第1条语句得到的结果是一个无法打印的字符,因此没有展示。
String.fromCharCode(134067); String.fromCodePoint(134067); //"𠮳"
五、解析字符串
ES6增强了JavaScript解析字符串的能力,新增了3个检索子串的方法(如表5所示),它们都返回布尔值。在某些场景,这些方法是indexOf()的理想替代品。
表5 新的检索方法
方法 | 功能描述 |
includes() | 判断子串是否存在于字符串中 |
startsWith() | 判断子串是否存在于字符串的头部 |
endsWith() | 判断子串是否存在于字符串的尾部 |
三个方法都能接收两个参数,先介绍第一个参数,表示要检索的子串,注意,子串不能是正则表达式,下面展示了只传一个参数时的情况。
var str = "My name is strick"; str.length; //17 str.includes("name"); //true str.startsWith("name"); //false str.endsWith("name"); //false
方法的第二个参数是一个可选值,它有两种含义。在includes()和startsWith()方法中用于指定检索的起始位置,默认值为0;而在endsWith()方法中用于指定原字符串str的长度,默认值为str.length。修改上面的代码,为startsWith()和endsWith()分别传入第二个参数,前者的值为3,后者的值为7,它们的结果都变成了true,如下所示。
str.startsWith("name", 3); //true str.endsWith("name", 7); //true
除了检索的新方法,ES6还提供了一个重复字符串的新方法:repeat(),它的参数是一个正整数,表示重复的次数,使用方法如下所示。
"name".repeat(2); //"namename"
最后介绍的是String对象的静态方法raw(),在第4篇模板字面量的标签模板中曾提到过。不过当时只强调了它是一个内置的标签模板,用于获取原始信息,但其实它也可以作为普通的函数来使用。只不过它的第一个参数得是一个包含raw属性的对象,raw属性的值既可以是数组也可以是字符串,第二个是可选的剩余参数,这些参数可插到指定位置,例如方法的第二个参数需要插到raw属性值中的第一和第二个元素之间,具体可参考下面的例子。
String.raw({raw: "abc"}, 0, 1, 2); //"a0b1c" //相当于 String.raw({raw: ["a", "b", "c"]}, 0, 1, 2); //"a0b1c"