字符串与正则表达式
字符串无疑在编程中最重要的类型之一,它们几乎出现在每个高级程序语言中,并且能高效的运用它们是开发者创造有用程序的基础,甚至于正则表达能给开发者额外的能力使用字符串也变得非常重要。记住这些事实以后,ECMAScript6的创建者通过对字符串和正则增加新属性和长久以来缺失的方法来达到提升。这章中我们将一一浏览这两种改变。
更好的Unicode支持
在ECMAScript6之前,javscript字符串以16位字符编码(UTF-16)。每连续的16位是一个编码单位代表一个字符。所有的字符串属性和方法如length属性,charAt()方法都是基于16位编码单位,16位通常是满足包含的任何字符,但是多亏字符扩展到Unicode以后就不再满足了。
UTF-16编码字
限制着字符长度到16位是不可能满足Uncode声明提供一个为全球每个字符提供一个独一无二的标识符的目的。那些全局独一无二的标识符被称作码字,从简单的数字0开始,码字正是你也许会想的字符编码,一个数字代表一个字符,一个字符编码必须让码字编译成内部对应关系(如UTF-16)的代码单元,一个码字可以由多个代码单元组成。
在UTF-16中第一个2^16^的码字代表一个单独16位代码单元,这个区域的被称作BMP,任何不在这个范围内的码字被当做在其中一个补充平面中,在补充平面中的码字不再能单独用16字节表示。在UTF-16通过引进代理编码对来解决这个问题,代理编码对表示一个码子用两个16字节编码单元表示,这就意味着任何一个单独字符在字符串中要么用一个(16字节BMP)或者两个(32字节)编码单元表示。
在ECAMScript5中,所有的字符串操作都是基于一个16位编码单元的,这意味着你会在UTF-16中包含代理编码对中得到意想不到的结果,如下例:
1 | var text = "𠮷"; |
这个单独的Unicode字符”𠮷”使用代理编码对表示的,并且在javascript中字符串操作都是把这个字符当做两个16位字符。这意味着:
- 当text的长度应该为1时结果为2
- 一个正则表达式在匹配单个字符串的时候失败,这是因为js把它当做两个字符
- 字符串方法chartAt()不能返回一个正确字符串,因为没有一个与之对应的可打印16位编码。
chatCodeAt()方法也不能识别对应字符属性。它只返回与编码单元对应的16位数字,但这就是你能在ECMAScript5中得到最为接近text真实值的方法。
在另一方面ECMAScript6,强制性UTF-16字符串编码来解决像上面的强调的问题,一系列的基于代理编码对标准编码,其余的这个章节将示例这些标准。
codePointAt()方法
在ECMAScript6中添加了一个完全支持UTF-16的方法codePointAt(),这个方法将根据给定的字符串位置检索对应Unicode编码,这个方法接受的是编码单元位置而不是字符位置并且返回一个整数,如下例代码所示:
1 | var text = "𠮷a"; |
当为BMP字符时codePointAt()方法和charCodeAt()返回是相同的,上例中第一个在text中字符是非BMP字符这意味着这个字符由两个编码单元组成,同时长度属性为3而不是2。当参数为0时方法charCodeAt()只返回第一个编码单元,但是codePointAt()不管字码包含是不是多个编码单元都将返回一个完整的字码。两个方法在参数为1(第一个字符的第二个编码单元)和2(’a’字符)时返回的都一致。
在字符上调用codePointAt()方法是最简单判定字符是否用一个或者两个编码单元(注:这里原作者写的是code points 但是应该是code units 编码单元),如下面这个函数:
1 | function is32Bit(c) { |
上例中用十六进制的FFFF表示16字节,所以任何超过这个数字的码字一定有两个编码单元构成的(总共32字节)
方法String.fromCodePoint()
当ECMAScript 提供一个方法将字符转换为数字,同样也会提供一个将数字转换到字符的。如下例:
1 | console.log(String.fromCodePoint(134071)); // "𠮷" |
你把String.fromCodePoint()当做String.fromCharCode()升级版就可以了。当字符在BMP范围内时两者都返回相同的结构。只有当字符范围超出了BMP时才会有区别。
方法normalize()
Unicode另一个有趣的地方是,不同的字符在排序或者比较的时候被认为相等。这里有两种不同的方式定义这些关系,首先规范性的相等指的是两个序列码点在各个方面都看起来都可以相互替换。如一个由两个字符拼接起来的的字符可以规范性的相等与另一个字符。第二种关系为兼容性的相等,两个兼容性相等的序列码点看起来不同但是可以在某些情况下互换。
因为这些关系某些本质上相同的文本可能包含不同的码点序列,如字符”æ”和包含两个字符”ae”也许可以互换但是严格上并不相等触发用某种方式标准化。
ECMAScript6 为Unicode标准化提供了normalize()方法,这个方法可以接受以下字符串参数进行Unicode标准化。
- “NFC” 默认
- “NFD”
- “NFKC”
- “NFKD”
解释这四种方式的差别超出了本书的范围了,只要记住当比较字符串时,两个字符串必须标准化为同一种格式后再比较,如下例:1
2
3
4
5
6
7
8
9
10
11
12
13var 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;
}
});
上面的代码将在value数组中的字符串同一标准化后才能进行准确的排序,你也可以在原始数组上调用normalize(),如下:
1
2
3
4
5
6
7
8
9
10
11
12 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;
}
});
再次说明,用相同的方式序列号是相当重要的,上面的例子用的是默认的NFC,但是你也可以轻松指定其中一种,如下:
1
2
3
4
5
6
7
8
9
10
11
12values.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中不止这些方法对Unicode字符串进行了改进,es6还增加了两个有用的语法。
正则表达式中u修饰符
通过正则表达式你可以对字符串完成许多普通的操作,但是记住,正则表达式是以16位字节的编码单元代表一个字符作为前提的,为了解决这个问题ECMAScript6定义了表示Unicode标准的u修饰符。
实战修饰符u
当一个正则表达式设置了修饰符u时,就将模式从编码单元切换到字符模式。这意味着正则表达式能正确识别字符串中代理编码对,看下面代码:
1 | var text = "𠮷"; |
正则表达式/^.$/将匹配一个字符串只包含一个字符,当没有设置u修饰符的时候,正则表达式基于编码单元匹配,所以日语字符(用两个编码单元表示)将不会被这个正则表达式匹配,一旦设置u修饰符后正则表达式基于字符而不是编码单元匹配所以能匹配日语字符。
计算码位数量
不幸的是,ECMAScript6并没提供计算字符串的码点数方法,但是通过u修饰符,你可以使用正则表达式计算出来:
1 | function codePointLength(text) { |
这个例子中调用match()来匹配text,在Unicode中的字符串”abc”和”𠮷bc”都包含3个字符,所以长度也是3.
检查u修饰符支持程度
既然u修饰符时新的语法变化,在不兼容ECMAScript6的js引擎中使用将会导致一个语法错误。最安全的检查支持程度的函数如下:
1 | function hasRegExpU() { |
这个方法中在RegExp构造器总使用u修饰符参数,在支持这个语法的老js引擎中也会正确,但是在不支持的引擎中将会报错。
其他关于字符串的改变
js的字符串总是落后与其他相似特性的语言,如只有在ECMACript5中strings最终得到了trim()方法,而在ES6中将会用新的方法继续扩展解析字符串能力。
识别子字符串的方法
在js中第一个被引进来判定字符串是否包含其他字符串的方法,但在ES6中包含如下3个方法,他们被设计成相同的作用:
- 方法includes() 包含指定文本返回true否则返回false
- 方法startWith() 包含以指定文本开始的String返回true否则返回false
- 方法endWith() 包含以指定文本结束的String返回true否则返回false
每个方法接受两个参数:一个搜索文本和一个可选的搜索起点下标,当连个参数都提供时,includes()和startWith()从下标处开始匹配同时endsWith()从匹配字符长度减去下标处开始搜索,当第二个参数省略掉后,includes()和startWith()从字符串开头开始匹配,endWith()从最后开始匹配,事实上第二个参数将减少搜寻的数量,这里是例子:
1 | var msg = "Hello world!"; |
前六个示例中都没有设置第二个参数,所以如果需要的话它们将匹配整个字符串。最后三个示例中只匹配了部分字符串。msg.startsWith(“o”, 4)从msg下标4开始匹配。msg.endsWith(“o”,8)是从字符串下标4开始,因为字符串长度12减8等于4。msg.includes(“o”, 8)是从字符串下标8开始的这正是”world”中的”r”。
这几个方法最终都返回boolean值,如果你想要获取实际匹配的下标可以用indexOf()或者lastIndexOf()
repeat()方法
ES6对字符串新加了一个repeat()方法,这个方法接受一个数字参数表示这个字符串重复的次数,它会返回一个规定重复次数新字符串。如下例:
1 | console.log("x".repeat(3)); // "xxx" |
这个方法比上面所有的方法简单,在操作文本时尤其有用,特别是在代码格式化工具中的创建缩减基本,像下面:
1 | // indent using a specified number of spaces |
调用第一个repeat()时创建了4个空格的字符串,然后用变量indentLevel跟踪缩减基本,最后再调用repeat()加上递增变量indentLevel控制空格的个数。
其他关于正则表达式的变化
在javascript中正则表达式对于操作字符串是相当重要,像这门语言的其他部分,它们在最近的版本中都没有太多的变化,然而ES6也随着字符串的改变而改变更新。