本節深入探討字串內部結構。如果你計畫處理表情符號、罕見的數學或象形文字,或其他罕見符號,這些知識將對你有所幫助。
正如我們所知,JavaScript 字串是基於 Unicode:每個字元都由 1-4 個位元組的位元組序列表示。
JavaScript 允許我們透過指定其十六進位 Unicode 碼來將字元插入字串,並使用以下三種表示法之一
-
\xXX
XX
必須是兩個十六進位數字,其值介於00
和FF
之間,然後\xXX
是 Unicode 碼為XX
的字元。由於
\xXX
表示法僅支援兩個十六進位數字,因此它只能用於前 256 個 Unicode 字元。這前 256 個字元包括拉丁字母、大多數基本語法字元和一些其他字元。例如,
"\x7A"
與"z"
(UnicodeU+007A
)相同。alert( "\x7A" ); // z alert( "\xA9" ); // ©, the copyright symbol
-
\uXXXX
XXXX
必須是恰好 4 個十六進位數字,其值介於0000
和FFFF
之間,然後\uXXXX
是 Unicode 碼為XXXX
的字元。字元 Unicode 值大於
U+FFFF
時,也可以用這種表示法表示,但這種情況下,我們需要使用所謂的代理對(我們將在本章稍後討論代理對)。alert( "\u00A9" ); // ©, the same as \xA9, using the 4-digit hex notation alert( "\u044F" ); // я, the Cyrillic alphabet letter alert( "\u2191" ); // ↑, the arrow up symbol
-
\u{X…XXXXXX}
X…XXXXXX
必須是 1 到 6 位元組的十六進制值,介於0
和10FFFF
(Unicode 定義的最高碼點)之間。這種表示法讓我們可以輕鬆表示所有現有的 Unicode 字元。alert( "\u{20331}" ); // 佫, a rare Chinese character (long Unicode) alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode)
代理對
所有常用的字元都有 2 位元組的碼(4 個十六進制數字)。大多數歐洲語言的字母、數字和基本統一 CJK 表意文字集(CJK - 來自中文、日文和韓文寫作系統)都有 2 位元組的表示法。
最初,JavaScript 基於 UTF-16 編碼,每個字元僅允許 2 位元組。但 2 位元組僅允許 65536 種組合,這不足以表示 Unicode 的每個可能符號。
因此,需要超過 2 位元組的罕見符號會使用稱為「代理對」的 2 位元組字元對編碼。
作為副作用,此類符號的長度為 2
alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY
alert( '𩷶'.length ); // 2, a rare Chinese character
這是因為在建立 JavaScript 時,代理對並不存在,因此語言無法正確處理它們!
我們實際上在上面每個字串中都有單一符號,但 length
屬性顯示的長度為 2
。
取得符號也可能很棘手,因為大多數語言功能將代理對視為兩個字元。
例如,我們可以在輸出中看到兩個奇異字元
alert( '𝒳'[0] ); // shows strange symbols...
alert( '𝒳'[1] ); // ...pieces of the surrogate pair
代理對的片段彼此沒有意義。因此,上面範例中的警示實際上顯示垃圾。
技術上來說,代理對也可以透過其碼來偵測:如果一個字元的碼在 0xd800..0xdbff
範圍內,則它是代理對的第一部分。下一個字元(第二部分)的碼必須在 0xdc00..0xdfff
範圍內。這些範圍由標準專門保留給代理對。
因此,在 JavaScript 中新增了 String.fromCodePoint 和 str.codePointAt 方法來處理代理對。
它們基本上與 String.fromCharCode 和 str.charCodeAt 相同,但它們正確處理了代理對。
我們可以在這裡看到差異
// charCodeAt is not surrogate-pair aware, so it gives codes for the 1st part of 𝒳:
alert( '𝒳'.charCodeAt(0).toString(16) ); // d835
// codePointAt is surrogate-pair aware
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, reads both parts of the surrogate pair
也就是說,如果我們從位置 1 取用(這裡相當不正確),那麼它們都只會傳回代理對的第二部分
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3
alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3
// meaningless 2nd half of the pair
您將在章節 可迭代物 的後續部分中找到更多處理代理對的方法。可能也有專門的函式庫,但沒有任何著名的函式庫建議在此處使用。
我們不能只在任意位置分割字串,例如,使用 `str.slice(0, 4)` 並期望它成為一個有效的字串,例如
alert( 'hi 😂'.slice(0, 4) ); // hi [?]
在輸出中,我們可以看到一個垃圾字元(笑臉代理對的第一個部分)。
如果您打算可靠地使用代理對,請注意這一點。這可能不是一個大問題,但至少您應該了解會發生什麼情況。
變音符號和正規化
在許多語言中,有些符號是由基本字元與其上方/下方的符號組成。
例如,字母 a
可以是這些字元的基礎字元:àáâäãåā
。
大多數常見的「複合」字元在 Unicode 表中都有自己的代碼。但並非所有字元都有,因為可能的組合太多。
為了支援任意組合,Unicode 標準允許我們使用多個 Unicode 字元:基本字元後接一個或多個「標記」字元來「裝飾」它。
例如,如果我們有 S
後接特殊「上方點」字元(代碼 \u0307
),它會顯示為 Ṡ。
alert( 'S\u0307' ); // Ṡ
如果我們需要在字母上方(或下方)加上額外的標記,沒問題,只要加上必要的標記字元即可。
例如,如果我們附加一個「下方點」字元(代碼 \u0323
),那麼我們將得到「上方和下方都有點的 S」:Ṩ
。
例如
alert( 'S\u0307\u0323' ); // Ṩ
這提供了很大的靈活性,但也帶來了一個有趣的問題:兩個字元在視覺上可能看起來相同,但使用不同的 Unicode 組合表示。
例如
let s1 = 'S\u0307\u0323'; // Ṩ, S + dot above + dot below
let s2 = 'S\u0323\u0307'; // Ṩ, S + dot below + dot above
alert( `s1: ${s1}, s2: ${s2}` );
alert( s1 == s2 ); // false though the characters look identical (?!)
為了解決這個問題,有一個「Unicode 正規化」演算法,可以將每個字串轉換為單一的「正規」形式。
它是由 str.normalize() 實作的。
alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true
有趣的是,在我們的狀況下,normalize()
實際上會將一個由 3 個字元組成的序列轉換成一個字元:\u1e68
(帶有兩個點的 S)。
alert( "S\u0307\u0323".normalize().length ); // 1
alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true
實際上,這並非總是如此。原因在於符號 Ṩ
「相當常見」,因此 Unicode 建立者將它包含在主表格中,並給予它代碼。
如果您想進一步了解正規化規則和變體,它們在 Unicode 標準的附錄中有說明:Unicode 正規化表單,但對於大多數實際用途,本節中的資訊就已足夠。
留言
<code>
標籤;若要插入多行程式碼,請將它們包覆在<pre>
標籤中;若要插入超過 10 行的程式碼,請使用沙盒(plnkr、jsbin、codepen…)