學習如何在 JavaScript 中處理 Unicode,瞭解 Emoji 的組成,ES6 的改進以及在處理 Unicode 時可能遇到的一些問題。
- 源文件的 Unicode 編碼
- JavaScript 在內部如何使用 Unicode
- 在字符串中使用 Unicode
- 歸一化
- Emoji
- 獲取字符串的正確長度
- ES6 Unicode 編碼點转義
- 編碼 ASCII 字節
Unicode 編碼的源文件
如果沒有指定其他方式,瀏覽器會假設任何程序的源代碼都是以本地字符集編寫的,而字符集因國家而異,可能會導致意外問題。因此,設置任何 JavaScript 文檔的字符集是很重要的。
如何指定其他字符集,尤其是 UTF-8,這是網絡上最常見的文件編碼方式?
如果文件包含BOM字符,那將優先確定字符集。在網上可以找到許多不同的意見,有人說不建議在 UTF-8 中使用 BOM,有些編輯器甚至不會添加 BOM。
這是Unicode標準所說的:
… 使用 BOM 對於 UTF-8 既不是必需的,也不是建議的,但在轉換使用了 BOM 的其他編碼形式的 UTF-8 數據的上下文中可能會遇到 BOM,或者在 BOM 作為 UTF-8 簽名使用的情況下可能會遇到 BOM。
這是 W3C 所說的:
在 HTML5 中,瀏覽器需要識別 UTF-8 BOM 並使用它檢測頁面的編碼,主要瀏覽器的最新版本在使用 UTF-8 編碼的頁面時也可以正常處理 BOM。- https://www.w3.org/International/questions/qa-byte-order-mark
如果使用 HTTP(或 HTTPS)獲取文件,Content-Type header 可以指定字符集:
Content-Type: application/javascript; charset=utf-8
如果沒有設置,則使用 script
標簽的 charset
屬性作為回退:
<script src="./app.js" charset="utf-8">
如果也沒有設置,則使用文檔字符集的元標記:
...
<head>
<meta charset="utf-8" />
</head>
...
兩種情況下的字符集屬性不區分大小寫(查看規範)
所有這些都在 RFC 4329 “Scripting Media Types” 中定義。
公共庫通常應避免在其代碼中使用 ASCII 字符集之外的字符,以避免使用不同於原始字符集的編碼加載它,從而引起問題。
JavaScript 如何在內部使用 Unicode
雖然 JavaScript 源文件可以使用任何類型的編碼,但 JavaScript 在執行之前會將其內部轉換為 UTF-16。
JavaScript 字符串都是 UTF-16 序列,正如 ECMAScript 標準所說:
當字符串包含實際文本數據時,每個元素都被視為是單個 UTF-16 代碼單元。
在字符串中使用 Unicode
可以使用格式 \uXXXX
在任何字符串內部添加 Unicode 序列:
const s1 = '\u00E9' //é
可以通過組合兩個 Unicode 序列來創建一個序列:
const s2 = '\u0065\u0301' //é
請注意,儘管兩者都生成了重音 e,但它們是兩個不同的字符串,並且 s2 被視為 2 個字符長度:
s1.length //1
s2.length //2
當你嘗試在文本編輯器中選擇該字符時,你需要通過它兩次,因為第一次按箭頭鍵選擇時,它只選擇了一半元素。
你可以通過將 Unicode 字符與普通字符結合來編寫字符串,因為在內部實際上它們是相同的:
const s3 = 'e\u0301' //é
s3.length === 2 //true
s2 === s3 //true
s1 !== s3 //true
歸一化
Unicode 正規化是消除字符表示中的歧義以幫助比較字符串的過程。例如,在上面的示例中:
const s1 = ‘\u00E9’ //é const s3 = ’e\u0301’ //é s1 !== s3
ES6/ES2015 在 String 原型上引入了 normalize() 方法,所以我們可以這樣做:
s1.normalize() === s3.normalize() //true
Emoji
Emoji 很有趣,它們是 Unicode 字符,因此它們完全可以在字符串中使用:
const s4 = ‘🐶’
Emoji 屬於星位字符,屬於第一個基本多語言平面(BMP)之外的字符,由於這些星位字符在 16 位元上無法表示,因此 JavaScript 需要使用 2 個字符的組合來表示它們。
代表🐶 符號的 UTF-16 編碼是 U+1F436
,傳統上編碼為 \uD83D\uDC36
(稱為代理對)。有一個公式可以計算出這個編碼,但這是一個相當高級的主題。
有些 Emoji 是通過將其他 Emoji 組合在一起創建的。你可以在這個列表https://unicode.org/emoji/charts/full-emoji-list.html 中找到它們,並注意具有多個項目的 Unicode 符號列中的符號。
👩❤️👩 由一個單獨的字符串中的 👩( \uD83D\uDC69
),❤️(\u200D\u2764\uFE0F\u200D
) 和另一個 👩( \uD83D\uDC69
) 串連在一起:\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC69
這個 Emoji 無法計算為 1 個字符。
獲取字符串的正確長度
如果你嘗試執行:
‘👩❤️👩’.length
你會得到 8 的結果,因為 length
函數計算的是單個 Unicode 碼點。
同樣,迭代它有點有趣:
奇怪的是,在密碼字段中粘貼這個表情符號時,它會被計算為 8 次,可能在一些系統中它是成為有效密碼的一種方式。
如何獲取包含 Unicode 字符的字符串的“真實”長度?
在 ES6+ 中一種簡單的方法是使用 spread operator:
;[…‘🐶’].length //1
也可以使用 Mathias Bynens 的 Punycode 库:
require(‘punycode’).ucs2.decode(‘🐶’).length //1
(Punycode 也可以將 Unicode 轉換為 ASCII)
請注意,由其他 Emoji 組合而成的 Emoji 仍然會給出錯誤的計數:
require(‘punycode’).ucs2.decode(‘👩❤️👩’).length //6 […‘👩❤️👩’].length //6
然而,如果字符串具有組合標記,這仍然不會給出正確的計數。可以查看這個 Glitch https://glitch.com/edit/#!/node-unicode-ignore-marks-in-length 作為一個例子。
(你可以在這裡生成自己的奇怪文本,加上標記:https://lingojam.com/WeirdTextGenerator)
注意,長度不是唯一需要注意的事情,即使是反轉字符串 https://mathiasbynens.be/notes/javascript-unicode#reversing-string 如果處理不當也容易出錯。
ES6 Unicode 編碼點轉義
ES6/ES2015 引入了一種表示星位字符(需要多於 4 個字符的任何 Unicode 編碼點)的方法,該方法使用圓括號括起來的代碼:
‘\u{XXXXX}’
狗 🐶 符號,它的 Unicode 是 U+1F436
,可以表示為 \u{1F436}
而不必結合兩個不相關的 Unicode 編碼點,就像我們之前展示的 \uD83D\uDC36
一樣。
但是,length
的計算仍然不正確,因為內部會將其轉換為上面顯示的代理對。
編碼 ASCII 字符
可以使用特殊的轉義字符 \x
來編碼前 128 個字符,它只接受 2 個字符:
‘\x61’ // a ‘\x2A’ // *
這僅適用於 \x00
到 \xFF
之間的 ASCII 字符集。