doclist 阅读(11) 评论(0)

  在介绍字符串之前,有必要先了解一点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"