1. 程式人生 > >ES6躬行記(9)——字符串

ES6躬行記(9)——字符串

羅馬 使用方法 也有 log string nic 有時 字符 存儲

  在介紹字符串之前,有必要先了解一點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)——字符串