摘要:我們得從原因理解起。這個(gè)編碼位置是唯一的。為了確保其組織性,把這個(gè)範(fàn)圍的編碼區(qū)分成個(gè)區(qū)段,各自由個(gè)編碼組成。由進(jìn)制格式的個(gè)位元組成代表一個(gè)編碼位置。跳脫序列可以被用來(lái)表示編碼位置從到。
為了理解 ES6 到底對(duì)於 Unicode 萬(wàn)國(guó)碼有哪些新的支援。我們得從原因理解起。
Javascript 在處理 Unicode 時(shí)很有多問(wèn)題關(guān)於 Javascript 處理 Unicode 的方式...至少可以說(shuō)是很奇怪。這篇文章闡述在 Javascript 中存取 Unicode 的痛點(diǎn)以及 ES6 如何改善這個(gè)問(wèn)題。
Unicode 基礎(chǔ)在我們深入探討 Javascript 之前,讓我們先確認(rèn)當(dāng)我們談到 Unicode 的時(shí)候說(shuō)的是相同的事情。
有關(guān) Unicode 的觀念其實(shí)非常簡(jiǎn)單,把它想成一個(gè)資料庫(kù),存取著您能想到的所有文字符號(hào),且每一個(gè)文字符號(hào)都對(duì)應(yīng)著一組數(shù)字。這個(gè)數(shù)字就叫編碼位置(Code point),也有人稱碼點(diǎn) 代碼點(diǎn)。這個(gè)編碼位置是唯一的。透過(guò)這種方式可以簡(jiǎn)單的存取特定文字符號(hào)而不用直接輸入符號(hào)本身。
例如:
A = U+0041
a = U+0061
? = U+00A9
? = U+2603
? = U+1F4A9
編碼位置通常使用 16 進(jìn)制的格式,位元左邊捕 0 到至少 4 位,使用 U+ 當(dāng)作前綴字。
編碼可能的範(fàn)圍從 U+0000 到 U+10FFFF 超過(guò) 110 萬(wàn)個(gè)符號(hào)。為了確保其組織性,Unicode 把這個(gè)範(fàn)圍的編碼區(qū)分成 17 個(gè)區(qū)段,各自由 65536 個(gè)編碼組成。
如果你曾經(jīng)看過(guò) Wiki 百科上的翻譯,他翻成平面,由 17 個(gè)平面組成。
第一個(gè)平面稱作基本多文種平面 Basic Multilingual Plane, 簡(jiǎn)稱BMP。這大概是最重要的一個(gè)。它包含了大部份常用的字符。一般使用英文的情況下您不會(huì)需要 BMP 以外的編碼來(lái)編輯文件。
BMP 以外剩下大概 1 百萬(wàn)個(gè)符號(hào)屬於補(bǔ)充平面(Supplementary planes or Astral planes)
補(bǔ)充平面的字非常好辨別: 如果某個(gè)字符需要超過(guò) 4 位元的 16 進(jìn)制來(lái)表示那它就屬於補(bǔ)充平面。
現(xiàn)在我們有了對(duì) Unicode 的基本認(rèn)識(shí)了。來(lái)看看如何應(yīng)用到 Javascript 的字串。
跳脫序列(Escape sequence)console.log("x41x42x43"); // "ABC" console.log("x61x62x63"); // "abc"
這個(gè)東西術(shù)語(yǔ)叫做 16 進(jìn)制的跳脫序列(字元)。由 16 進(jìn)制格式的 2 個(gè)位元組成代表一個(gè)編碼位置。舉例來(lái)說(shuō) x41 代表 U+0041。
跳脫序列可以被用來(lái)表示編碼位置從 U+0000 到 U+00FF。
另外一種常見(jiàn)的跳脫序列的表示類型如下
console.log("u0041u0042u0043"); // "ABC" console.log("I u2661 JavaScript"); // "I ? JavaScript"
這種格式被稱作萬(wàn)國(guó)碼跳脫序列,算了!還是記英文吧!Unicode escape squences 由16 進(jìn)制格式 4 個(gè)位元組成精準(zhǔn)的表達(dá)編碼位置,舉例來(lái)說(shuō): u2661 表示 U+2661 這種跳脫序列可以用來(lái)表示 U+0000 到 U+FFFF 範(fàn)圍的萬(wàn)國(guó)碼 Unicode 等於是整個(gè)基本多文種平面(BMP)
那麼..其他平面呢? 我們需要大於 4 位元來(lái)表示其他編碼位置啊! 我們要如何使用跳脫序列呈現(xiàn)它們?
ES6 引進(jìn)了新類型的跳脫序列: Unicode code point escapes 讓事情變得比較簡(jiǎn)單
舉例來(lái)說(shuō):
console.log("u{41}u{42}u{43}"); // "ABC" console.log("u{1F4A9}"); // "?" U+1F4A9
在大括號(hào)之間您可以使用 6 位元的 16 進(jìn)制,這麼一來(lái)就足夠表示所有的 Unicode 編碼。
所以透過(guò)這種類型的跳脫序列您可以輕易的輸出任何您想用的符號(hào)
為了兼容 ES5 和舊有的環(huán)境,一個(gè)不是很好的解決方案出現(xiàn)了,就是使用成對(duì)編碼來(lái)代理
console.log("uD83DuDCA9"); // "?" U+1F4A9
在這種情況下每一個(gè)跳脫字元(跳脫序列)代表一半的編碼位置,2 個(gè)代理編碼組成一個(gè)字符的 Code point。
注意到這個(gè)編碼沒(méi)辦法很直覺(jué)的看出其規(guī)則,這是有一套公式的
例如一個(gè) C 字符大於 0xFFFF 就得對(duì)應(yīng)到
H = Math.floor((C - 0x10000) / 0x400) + 0xD800 L = (C - 0x10000) % 0x400 + 0xDC00
之後我們提到代理編碼指的就是兩個(gè)編碼其中之一
第一個(gè)的是 H, 第二個(gè)是 L
要反轉(zhuǎn)回來(lái)則是
C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x10000
透過(guò)這種代理編碼的機(jī)制所有補(bǔ)充平面的編碼位置(U+010000 - U+10FFFF) 都可以使用。不過(guò)使用單一跳脫字元來(lái)表示 BMP 裡面的字,兩個(gè)跳脫字元(代理編碼)來(lái)處理剩下補(bǔ)充平面的字很容易讓人搞混,造成很多惱人的後果。
計(jì)算 JavaScript 字串的文字(符號(hào))假設(shè)您想計(jì)算一個(gè)字串的文字有幾個(gè),您會(huì)怎麼處理呢?
直覺(jué)的想法大概是使用 length
console.log("A".length); // 1 console.log("A" == "u0041"); // true
上面這個(gè)例子 length 剛好是字元的數(shù)量,說(shuō)有 1 個(gè)文字這很合理。
很顯然的我們每一個(gè)文字只需要一個(gè)跳脫字元,但實(shí)際上卻不是這樣。例如:
console.log("?".length); // U+1D400 注意這不只是全形A // 2 console.log("?" == "uD835uDC00"); // true console.log("?".length) // U+1D401 // 2 console.log("?" == "uD835uDC01"); // true console.log("?".length); // 2 console.log("?" == "uD83DuDCA9"); // true
在內(nèi)部 JavaScript 把補(bǔ)充平面的字符視為兩個(gè)跳脫字元(代理編碼)表示一個(gè)字。如果您在 ES5 兼容的瀏覽器輸出您會(huì)看到他把他視為兩個(gè)跳脫字元 length 為 2 ,人們對(duì)於字面上只顯示一個(gè)字但是 length 卻為 2 會(huì)產(chǎn)生困惑。
計(jì)算補(bǔ)充平面裡的文字回到剛剛的問(wèn)題,那我們?nèi)绾斡?jì)算 JS 字串中有幾個(gè)字?
這個(gè)小技巧針對(duì)代理編碼做處理,當(dāng)我們認(rèn)出這兩個(gè)跳脫字元會(huì)組成一個(gè)字的時(shí)候只計(jì)算一次
var regexAstralSymbols = /[uD800-uD8FF][uDC00-uDCFF]/g; function countSymbols(string) { return string.replace(regexAstralSymbols, "_").length; }
或者您也可以使用 Punycode.js,punycode.ucs2.decode 方法可以取得一個(gè)字串並回傳一個(gè)包含 Unicode 編碼位置的陣列。如此一來(lái)您就可以計(jì)算幾個(gè)字了。
在 ES6 您可以透過(guò) Array.from 做類似的事情,透過(guò)使用字串的 iterator 來(lái)切割字串成為一個(gè)陣列
var astral = Array.from("???"); console.log(astral); console.log(astral.length); // 3
或者使用 ...
console.log([..."???"].length) // 3
使用上面提到的這些方法,我們可以解決計(jì)算幾個(gè)字的問(wèn)題。
看起來(lái)一樣,但卻不一樣但是如果我們開始去賣弄我們從文章中學(xué)到的知識(shí),計(jì)算文字的數(shù)量甚至更多複雜的操作例如下面這段程式碼
console.log("ma?ana" == "ma?ana"); // false
JavaScript 會(huì)告訴我們這兩個(gè)字串不一樣,但看起來(lái)明明就一樣。
試著到這個(gè)網(wǎng)址看看
Javascript escapes 工具告訴我們其中的不同
console.log("maxF1ana" == "manu0303ana"); // false console.log("maxF1ana".length); // 6 console.log("manu0303ana".length); // 7
第一個(gè)字串包含的是 U+00F1 是一個(gè)拉丁字小寫 N 加上波浪號(hào)。而第二個(gè)字串裡面的是 U+006E 拉丁字小寫 N 加上 U+0303 波浪號(hào),兩個(gè)編碼合體成一個(gè)字。這樣你明白了為什麼他們不一樣了吧。
然而如果我們希望兩個(gè)字串計(jì)算結(jié)果都會(huì)是 6 個(gè)字呢?
在 ES6 也相當(dāng)直覺(jué)
var normalized = "ma?ana".normalize("NFC"); // 把字串標(biāo)準(zhǔn)化 console.log(Array.from(normalized).length); // 6 console.log([...normalized].length); // 6
這個(gè)標(biāo)準(zhǔn)化 normalize 方法是內(nèi)建 String.prototype 的方法,他會(huì)根據(jù)Unicode normalization的規(guī)則執(zhí)行,找出那些字的差異,如果找到那種由兩個(gè)代理編碼組成的字卻長(zhǎng)得跟另一單一編碼位置一樣的字,它會(huì)把它轉(zhuǎn)成單一的那種編碼。
[..."ma?ana"].lenght // U+00F1 // 6 [..."ma?ana"].length // U+006E + U+0303 // 6 // 透過(guò)程式碼驗(yàn)證 var normalized = "ma?ana".normalize("NFC"); console.log(normalized[2] + " = " + normalized.charCodeAt(2)) // ? = 241, 241 轉(zhuǎn)成 16 進(jìn)制 F1
為了向下相容 ES5 和舊環(huán)境可以使用這個(gè)Polyfill
事情還很複雜 - 計(jì)算其他組合式的代理編碼光上面這些還不夠完美,編碼位置可以有多種組合方式其結(jié)果看起來(lái)是一個(gè)字,但是卻沒(méi)有標(biāo)準(zhǔn)化的格式(或者說(shuō)沒(méi)有相同樣子的字取代)。
這種時(shí)後 normalization 就幫不上忙了。
大部份開發(fā)者應(yīng)該很少遇到這類問(wèn)題吧???
var q = "qu0307u0323".normalize("NFC") // q?? // 經(jīng)過(guò) normalize 還是 qu0307u0323 console.log([...q].length); // 是 3 不是 1 console.log([..."Z??????????????????A????????L?????G??????????????????????!????????????????"].length); // 是 74 不是 6
此時(shí)您可以使用正規(guī)式來(lái)移除那些組合的符號(hào)
var sample = "Z??????????????????A????????L?????G??????????????????????!????????????????"; var pattern = /([