/

JavaScript 中的 Unicode

JavaScript 中的 Unicode

學習如何在 JavaScript 中處理 Unicode,瞭解 Emoji 的組成,ES6 的改進以及在處理 Unicode 時可能遇到的一些問題。

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 可以指定字符集:

1
Content-Type: application/javascript; charset=utf-8

如果沒有設置,則使用 script 標簽的 charset 屬性作為回退:

1
<script src="./app.js" charset="utf-8">

如果也沒有設置,則使用文檔字符集的元標記:

1
2
3
4
5
...
<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 序列:

1
const s1 = '\u00E9' //é

可以通過組合兩個 Unicode 序列來創建一個序列:

1
const s2 = '\u0065\u0301' //é

請注意,儘管兩者都生成了重音 e,但它們是兩個不同的字符串,並且 s2 被視為 2 個字符長度:

1
2
s1.length //1
s2.length //2

當你嘗試在文本編輯器中選擇該字符時,你需要通過它兩次,因為第一次按箭頭鍵選擇時,它只選擇了一半元素。

你可以通過將 Unicode 字符與普通字符結合來編寫字符串,因為在內部實際上它們是相同的:

1
2
3
4
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 碼點。

同樣,迭代它有點有趣:

迭代 Emoji

奇怪的是,在密碼字段中粘貼這個表情符號時,它會被計算為 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 字符集。

tags: [“JavaScript”, “Unicode”, “Emojis”, “ES6”, “Encoding”]