ES6(四):關(guān)于Unicode的相關(guān)擴(kuò)展

前面的話(huà)


JS中的字符串類(lèi)型是由引號(hào)括起來(lái)的一組由16位Unicode字符組成的字符序列。在過(guò)去,16位足以包含任何字符,直到Unicode引入了擴(kuò)展字符集,編碼規(guī)則不得不進(jìn)行變更。本文將詳細(xì)介紹ES6關(guān)于Unicode的相關(guān)擴(kuò)展

概述

Unicode的目標(biāo)是為世界上每一個(gè)字符提供唯一標(biāo)識(shí)符,唯一標(biāo)識(shí)符稱(chēng)為碼位或碼點(diǎn)(codepoint)。而這些碼位是用于表示字符的,又稱(chēng)為字符編碼(characterencode)

ES6之前,JS 的字符串以16 位字符編碼(UTF-16)為基礎(chǔ)。每個(gè)16 位序列(相當(dāng)于2個(gè)字節(jié))是一個(gè)編碼單元(codeunit),可簡(jiǎn)稱(chēng)為碼元,用于表示一個(gè)字符。字符串所有的屬性與方法(如length屬性與charAt()方法等)都是基于16位序列

【BMP】

最常用的Unicode字符使用16位序列編碼字符,屬于“基本多語(yǔ)種平面”(Basic Multilingual Plane BMP),也稱(chēng)為“零斷面”(plan0), 是Unicode中的一個(gè)編碼區(qū)段,編碼介于U+0000——U+FFFF之間。超過(guò)這個(gè)范圍的碼位則要?dú)w屬于某個(gè)輔助平面或稱(chēng)為擴(kuò)展平面(supplementaryplane),其中的碼位僅用16位就無(wú)法表示了
為此,UTF-16引入了代理對(duì)(surrogatepairs),規(guī)定用兩個(gè)16位編碼來(lái)表示一個(gè)碼位。

字符串里的字符有兩種:

  • 一種由一個(gè)碼元(共16 位)來(lái)表示BMP字符,
  • 另一種用兩個(gè)碼元(共32 位)來(lái)表示輔助平面字符

大括號(hào)表示

JavaScript 允許采用\uxxxx形式表示一個(gè)字符,其中xxxx表示字符的Unicode碼位

// "a"
console.log("\u0061");

但是,這種表示法只限于碼位在\u0000~\uFFFF之間的字符。超出這個(gè)范圍的字符,必須用兩個(gè)雙字節(jié)的形式表示

// "??"
console.log("\uD842\uDFB7");
// "?7"
console.log("\u20BB7");
  • 上面代碼表示,如果直接在\u后面跟上超過(guò)0xFFFF的數(shù)值(比如\u20BB7),JavaScript會(huì)理解成\u20BB+7。所以會(huì)顯示一個(gè)特殊字符,后面跟著一個(gè)7

ES6對(duì)這一點(diǎn)做出了改進(jìn),只要將碼位放入大括號(hào),就能正確解讀該字符

// "??"
console.log("\u{20BB7}");

// "ABC"
console.log("\u{41}\u{42}\u{43}");

let hello = 123;
// 123
console.log(hell\u{6F});

// true
console.log('\u{1F680}' === '\uD83D\uDE80');

上面代碼中,最后一個(gè)例子表明,大括號(hào)表示法與四字節(jié)的UTF-16 編碼是等價(jià)的。
有了這種表示法之后,JavaScript共有6種方法可以表示一個(gè)字符

'\z' === 'z'// true
'\172' === 'z'// true
'\x7A' === 'z'// true
'\u007A' === 'z'// true
'\u{7A}' === 'z'// true

字符編解碼

【codePointAt()】

ES6新增了完全支持UTF-16的方法codePointAt(),該方法接受編碼單元的位置而非字符位置作為參數(shù),返回與字符串中給定位置對(duì)應(yīng)的碼位,即一個(gè)整數(shù)值

var text = "??a";

console.log(text.charCodeAt(0));// 55362
console.log(text.charCodeAt(1));// 57271
console.log(text.charCodeAt(2));// 97
console.log(text.codePointAt(0));// 134071
console.log(text.codePointAt(1));// 57271
console.log(text.codePointAt(2));// 97
  • 對(duì)于BMP字符,codePointAt()方法的返回值與charCodeAt() 相同,如'a',都返回97
  • 對(duì)于輔助平面的32位字符,如'??',charCodeAt()和codePointAt()方法都分為兩部分返回
  • charCodeAt(0)和chatCodeAt(1)分別返回前16位和后16位的編碼;而codePointAt(0)和codePointAt(1)分別返回32位編碼及后16位的編碼
  • 判斷一個(gè)字符是否是BMP,對(duì)該字符調(diào)用codePointAt() 方法就是最簡(jiǎn)單的方法
function is32Bit(c) {    
  returnc.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("??" )); // true 
console.log(is32Bit("a")); // false

16位字符的上邊界用十六進(jìn)制表示就是FFFF,因此任何大于該數(shù)字的碼位必須用兩個(gè)碼元(共32位)表示

【String.fromCodePoint()】

ES5提供的String.fromCharCode方法,用于從碼位返回對(duì)應(yīng)字符,但是這個(gè)方法不能識(shí)別32位的UTF-16字符

ECMAScript通常會(huì)提供正反兩種方法??梢允褂?code>codePointAt()來(lái)提取字符串內(nèi)中某個(gè)字符的碼位,也可以借助String.fromCodePoint()根據(jù)給定的碼位來(lái)生成一個(gè)字符

console.log(String.fromCharCode(0x20bb7));//"?"
console.log(String.fromCodePoint(0x20bb7)); // "??"
console.log(String.fromCharCode(0x0bb7));// "?"
  • 上面代碼中,String.fromCharCode不能識(shí)別大于0xFFFF的碼位,所以0x20BB7就發(fā)生了溢出,最高位2被舍棄了,最后返回碼位U+0BB7對(duì)應(yīng)的字符,而不是碼位U+20BB7對(duì)應(yīng)的字符

如果String.fromCodePoint()方法有多個(gè)參數(shù),則它們會(huì)被合并成一個(gè)字符串返回

// true
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' 

可以將String.fromCodePoint() 視為 String.fromCharCode() 的完善版本。兩者處理BMP 字符時(shí)會(huì)返回相同結(jié)果,只有處理BMP 范圍之外的字符時(shí)才會(huì)有差異

for...of

對(duì)于32位的輔助平面字符來(lái)說(shuō),使用forforin循環(huán),可能得不到正確的結(jié)果

var s = '??a';
for(let chins) {
  console.log(s[ch]);
} //?//?//a

而for...of循環(huán)可以正確的識(shí)別32位的UTF-16字符

var s = '??a';
for(let ch of s) {
  console.log(ch);
}//??//a

normalize()

許多歐洲語(yǔ)言有語(yǔ)調(diào)符號(hào)和重音符號(hào)。為了表示它們,Unicode提供了兩種方法。一種是直接提供帶重音符號(hào)的字符,比如ǒ(\u01D1)。另一種是提供合成符號(hào)(combiningcharacter),即原字符與重音符號(hào)的合成,兩個(gè)字符合成一個(gè)字符,比如O(\u004F)ˇ(\u030C)合成ǒ(\u004F\u030C)

這兩種表示方法,在視覺(jué)和語(yǔ)義上都等價(jià),但是JavaScript不能識(shí)別

console.log('\u01D1'==='\u004F\u030C');//false
console.log('\u01D1'.length);// 1
console.log('\u004F\u030C'.length);// 2
  • 上面代碼表示,JavaScript將合成字符視為兩個(gè)字符,導(dǎo)致兩種表示方法不相等。

ES6提供字符串實(shí)例的normalize()方法,用來(lái)將字符的不同表示方法統(tǒng)一為同樣的形式,這稱(chēng)為Unicode正規(guī)化

console.log('\u01D1'==='\u01D1'.normalize());//true
console.log('\u01D1'=== '\u004F\u030C'.normalize());//true

normalize方法可以接受一個(gè)參數(shù)來(lái)指定normalize的方式,參數(shù)的四個(gè)可選值如下

  • 1. NFC,默認(rèn)參數(shù),表示“標(biāo)準(zhǔn)等價(jià)合成”(Normalization Form Canonical Composition),返回多個(gè)簡(jiǎn)單字符的合成字符。所謂“標(biāo)準(zhǔn)等價(jià)”指的是視覺(jué)和語(yǔ)義上的等價(jià)
console.log('\u01D1'==='\u01D1'.normalize("NFC"));//true
console.log('\u01D1'=== '\u004F\u030C'.normalize("NFC"));//true
  • 2. NFD,表示“標(biāo)準(zhǔn)等價(jià)分解”(Normalization Form Canonical Decomposition),即在標(biāo)準(zhǔn)等價(jià)的前提下,返回合成字符分解的多個(gè)簡(jiǎn)單字符
console.log('\u004F\u030C'==='\u01D1'.normalize("NFD"));//true
console.log('\u004F\u030C'=== '\u004F\u030C'.normalize("NFD"));//true
  • 3. NFKC,表示“兼容等價(jià)合成”(Normalization Form Compatibility Composition),返回合成字符。所謂“兼容等價(jià)”指的是語(yǔ)義上存在等價(jià),但視覺(jué)上不等價(jià),比如“囍”和“喜喜”。(這只是用來(lái)舉例,normalize方法不能識(shí)別中文。)

  • 4. NFKD,表示“兼容等價(jià)分解”(Normalization Form Compatibility Decomposition),即在兼容等價(jià)的前提下,返回合成字符分解的多個(gè)簡(jiǎn)單字符

在開(kāi)發(fā)國(guó)際化應(yīng)用時(shí),normalize() 方法非常有用。但normalize()方法目前不能識(shí)別三個(gè)或三個(gè)以上字符的合成。這種情況下,還是只能使用正則表達(dá)式,通過(guò)Unicode編號(hào)區(qū)間判斷

U修飾符

正則表達(dá)式可以完成簡(jiǎn)單的字符串操作,但默認(rèn)將字符串中的每一個(gè)字符按照16位編碼處理。為了解決這個(gè)問(wèn)題, ES6對(duì)正則表達(dá)式添加了u修飾符,含義為“Unicode模式”,用來(lái)正確處理大于\uFFFF的Unicode 字符。也就是說(shuō),會(huì)正確處理四個(gè)字節(jié)的UTF-16 編碼

/^\uD83D/u.test('\uD83D\uDC2A')// false
/^\uD83D/.test('\uD83D\uDC2A')// true

一旦為正則表達(dá)式設(shè)置了u 修飾符,正則表達(dá)式將會(huì)識(shí)別32位的輔助平面字符為1個(gè)字符,而不是兩個(gè)

【點(diǎn)號(hào)】

點(diǎn)(.)字符在正則表達(dá)式中,含義是除了換行符以外的任意單個(gè)字符。對(duì)于碼位大于0xFFFF的Unicode 字符,點(diǎn)字符不能識(shí)別,必須加上u修飾符

var text = "??";
console.log(text.length); // 2
console.log(/^.$/.test(text));//false
console.log(/^.$/u.test(text));//true

【大括號(hào)】

ES6 新增了使用大括號(hào)表示Unicode 字符,這種表示法在正則表達(dá)式中必須加上u修飾符,才能識(shí)別當(dāng)中的大括號(hào),否則會(huì)被解讀為量詞

/\u{61}/.test('a')// false
/\u{61}/u.test('a')// true
/\u{20BB7}/u.test('??')// true

【量詞】

使用u修飾符后,所有量詞都會(huì)正確識(shí)別碼點(diǎn)大于0xFFFF的Unicode 字符

/a{2}/.test('aa')// true
/a{2}/u.test('aa')// true
/??{2}/.test('????')// false
/??{2}/u.test('????')// true

【預(yù)定義模式】

u修飾符也影響到預(yù)定義模式,能否正確識(shí)別碼點(diǎn)大于0xFFFF的Unicode 字符

/^\S$/.test('??') // false
/^\S$/u.test('??')// true

【字符串長(zhǎng)度】

上面代碼的\S是預(yù)定義模式,匹配所有不是空格的字符。只有加了u修飾符,它才能正確匹配碼點(diǎn)大于0xFFFF的Unicode 字符

雖然ES6不支持字符串碼位數(shù)量的檢測(cè),length屬性仍然返回字符串編碼單元的數(shù)量。利用[\s\S],再加上u修飾符,就可以寫(xiě)出一個(gè)正確返回字符串長(zhǎng)度的函數(shù)

function codePointLength(text) {  
  var result = text.match(/[\s\S]/gu);  
  returnresult ? result.length : 0;
}
var s = '????';
console.log(s.length); // 4
console.log(codePointLength(s));// 2

【檢測(cè)支持】

u修飾符是語(yǔ)法層面的變更,嘗試在不兼容ES6JS 引擎中使用它會(huì)拋出語(yǔ)法錯(cuò)誤。如果要檢測(cè)當(dāng)前引擎是否支持u修飾符,最安全的方式是通過(guò)以下函數(shù)來(lái)判斷

function hasRegExpU() {    
  try {        
    var pattern =newRegExp(".", "u");   
    return true;
  } catch (ex) {        
    return false;
  }
}

這個(gè)函數(shù)使用了RegExp構(gòu)造函數(shù)并傳入字符串'u'作為參數(shù),該語(yǔ)法即使在舊版JS 引擎中也是有效的。但是,如果當(dāng)前引擎不支持u修飾符則會(huì)拋出錯(cuò)誤

其他章節(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容