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