ES6躬行記(9)——字符串
在介紹字符串之前,有必要先了解一點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"
ES6躬行記(9)——字符串