在介紹字元串之前,有必要先了解一點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 = "ud842udfb3";
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 = "u0075u0308";
mark1 === mark2; //false

  ES6新增了一個原型方法normalize(),可以將字元串標準化,修改上面的例子,就能得到相等的結果,如下所示。

mark1.normalize() === mark2.normalize(); //true

  normalize()方法可以接收一個字元串參數,但只有4個可選值(如表4所示),其中「NFC」是方法的默認值。

表4 標準化參數

  上表中的標準等價(Canonical Equivalence)和兼容等價(Compatibility Equivalence)都表示相同的字元或字元序列,並且前者是後者的一個子集。標準等價會保持視覺外觀和文本含義,前面字元「ü」的示例就用到了標準等價;而兼容等價會改變視覺外觀和文本含義,例如羅馬數字十二(Ⅻ)可由一個羅馬數十(Ⅹ)和兩個羅馬數一(Ⅰ)組成,兩者只有通過兼容等價的標準化處理後才能匹配成功,如下所示。

var digit1 = "u216B", //"Ⅻ"
digit2 = "u2169u2160u2160"; //"ⅩⅠⅠ"
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 新的檢索方法

  三個方法都能接收兩個參數,先介紹第一個參數,表示要檢索的子串,注意,子串不能是正則表達式,下面展示了只傳一個參數時的情況。

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"

推薦閱讀:

相关文章