本章為Unicode 簡介,以及如何在 JavaScript 中處理 Unicode。
第一份 Unicode 草案提案於 1988 年發布。此後持續進行相關工作,工作小組也擴大規模。Unicode Consortium於 1991 年 1 月 3 日成立
Unicode Consortium 是一家非營利公司,致力於開發、維護和推廣軟體國際化標準和資料,特別是 Unicode 標準 [...]
Unicode 1.0 標準的第一冊於 1991 年 10 月發布,第二冊於 1992 年 6 月發布。
字元的概念看似簡單,但其實包含許多面向。這也是 Unicode 如此複雜的原因。以下是重要的基本概念:
如果碼元大於單一位元組,則位元組順序很重要。BOM 是文字開頭的單一偽字元(可能編碼為多個碼元),它表示碼元是大端序(最高位元組優先)還是小端序(最低位元組優先)。沒有 BOM 的文字預設為大端序。BOM 也表示所使用的編碼;UTF-8、UTF-16 等編碼的 BOM 不同。此外,如果網頁瀏覽器沒有其他關於文字編碼的資訊,BOM 可作為 Unicode 的標記。然而,由於以下幾個原因,BOM 並不常使用:
Unicode 規格為每個字元指定了多種屬性,以下是其中一些:
名稱。英文名稱,由大寫字母 A–Z、數字 0–9、連字符 (-) 和 <space> 組成。兩個範例
碼點 的範圍最初為 16 位元。在 Unicode 版本 2.0 (1996 年 7 月) 中,它已擴充:現在分為 17 個平面,編號為 0 到 16。每個平面包含 16 位元 (十六進位表示法:0x0000–0xFFFF)。因此,在以下十六進位範圍中,四個底層數字以外的數字包含平面的數字。
平面 1–16 稱為補充平面或星體平面。
UTF-32 (Unicode 轉換格式 32) 是一種具有 32 位元碼單位的格式。任何碼點都可以由一個碼單位編碼,這使其成為唯一的定長編碼;對於其他編碼,編碼一個點所需的單位數會有所不同。
UTF-16 是一種具有 16 位元碼單位的格式,需要一到兩個單位來表示一個碼點。BMP 碼點可以用單個碼單位表示。較高的碼點是 20 位元 (16 乘以 16 位元),減去 0x10000 (BMP 的範圍) 之後。這些位元編碼為兩個碼單位 (一個所謂的代理對):
下表 (改編自 Unicode 標準 6.2.0,表 3-5) 說明位元如何分配
代碼點 | UTF-16 代碼單元 |
xxxxxxxxxxxxxxxx (16 位元) | xxxxxxxxxxxxxxxx |
pppppxxxxxxyyyyyyyyyy (21 位元 = 5+6+10 位元) | 110110qqqqxxxxxx 110111yyyyyyyyyy (qqqq = ppppp − 1) |
若要啟用此編碼配置,BMP 會有一個孔洞,其範圍為 0xD800–0xDFFF,且為未使用的代碼點。因此,前導代理、後置代理和 BMP 代碼點的範圍是分開的,讓解碼在發生錯誤時仍能健全。下列函數將代碼點編碼為 UTF-16 (稍後我們會看到使用範例)
function
toUTF16
(
codePoint
)
{
var
TEN_BITS
=
parseInt
(
'1111111111'
,
2
);
function
u
(
codeUnit
)
{
return
'\\u'
+
codeUnit
.
toString
(
16
).
toUpperCase
();
}
if
(
codePoint
<=
0xFFFF
)
{
return
u
(
codePoint
);
}
codePoint
-=
0x10000
;
// Shift right to get to most significant 10 bits
var
leadingSurrogate
=
0xD800
|
(
codePoint
>>
10
);
// Mask to get least significant 10 bits
var
trailingSurrogate
=
0xDC00
|
(
codePoint
&
TEN_BITS
);
return
u
(
leadingSurrogate
)
+
u
(
trailingSurrogate
);
}
UCS-2 是一個已棄用的格式,使用 16 位元代碼單元來表示 (僅限於) BMP 的代碼點。當 Unicode 代碼點的範圍擴展到 16 位元以上時,UTF-16 便取代了 UCS-2。
如果最高位元不是 0,則 0 之前的 1 的數量表示序列中有多少個代碼單元。初始代碼單元之後的所有代碼單元都有位元前綴 10。因此,初始代碼單元和後續代碼單元的範圍是分開的,這有助於從編碼錯誤中復原。
UTF-8 已成為最受歡迎的 Unicode 格式。最初,它之所以受歡迎是因為它向下相容於 ASCII。後來,它獲得了廣泛且一致的支援,橫跨作業系統、程式設計環境和應用程式,因此聲勢看漲。
JavaScript 處理 Unicode 原始碼的方式有兩種:內部 (在解析期間) 和外部 (在載入檔案時)。
在內部,JavaScript 程式碼被視為 UTF-16 編碼單位的序列。根據ECMAScript 規格的第 6 節
ECMAScript 原始碼文字以 Unicode 字元編碼中的字元序列表示,版本 3.0 或更新版本。[...] 為了本規格的目的,ECMAScript 原始碼文字假設為 16 位元編碼單位的序列。[...] 如果實際原始碼文字以 16 位元編碼單位以外的形式編碼,則必須將其處理為先轉換為 UTF-16。
在識別碼、字串文字和正規表示式文字中,任何編碼單位也可以透過 Unicode 逸出序列 \uHHHH
表示,其中 HHHH
是四個十六進位數字。例如:
> var f\u006F\u006F = 'abc'; > foo 'abc' > var λ = 123; > \u03BB 123
這表示您可以在文字和變數名稱中使用 Unicode 字元,而不用在原始碼中離開 ASCII 範圍。
在字串文字中,有另一種逸出:十六進位逸出序列,其中包含表示 0x00–0xFF 範圍內編碼單位的兩位數十六進位數字。例如:
> '\xF6' === 'ö' true > '\xF6' === '\u00F6' true
雖然 UTF-16 在內部使用,但 JavaScript 原始碼通常不會儲存在該格式中。當網頁瀏覽器透過 <script>
標籤載入原始檔時,它會如下確定編碼
否則,如果檔案是透過 HTTP(S) 載入,則 Content-Type
標頭可以透過 charset
參數指定編碼。例如
Content-Type: application/javascript; charset=utf-8
正確的媒體類型(以前稱為MIME 類型)JavaScript 檔案是 application/javascript
。但是,較舊的瀏覽器(例如 Internet Explorer 8 及更早版本)使用 text/javascript
最可靠。不幸的是,預設值屬性 type
的 <script>
標籤是 text/javascript
。至少您可以省略 JavaScript 的該屬性;包含它沒有好處。
<script>
標籤具有屬性 charset
,則使用該編碼。即使屬性 type
具有有效的媒體類型,該類型也不得具有參數 charset
(如前述 Content-Type
標頭中)。這可確保 charset
和 type
的值不會衝突。
否則,將使用文件編碼,其中包含 <script>
標籤。例如,這是 HTML5 文件的開頭,其中 <meta>
標籤宣告文件編碼為 UTF-8
<!doctype html>
<html>
<head>
<meta
charset=
"UTF-8"
>
...
強烈建議您始終指定編碼。如果您沒有指定,將使用特定語言環境的 預設編碼。換句話說,人們在不同國家會看到不同的檔案。只有最低的 7 位元在不同語言環境中相對穩定。
我的建議可總結如下
一些壓縮工具可以 將超出 7 位元的 Unicode 編碼點的原始碼轉換為「7 位元乾淨」的原始碼。它們透過將非 ASCII 字元替換為 Unicode 逸出字元來執行此操作。例如,以下呼叫 UglifyJS 會轉換檔案 test.js:
uglifyjs -b beautify=false,ascii-only=true test.js
檔案 test.js 如下所示
var
σ
=
'Köln'
;
UglifyJS 的輸出如下所示
var
\
u03c3
=
"K\xf6ln"
;
考慮以下負面範例。有一段時間,D3.js 程式庫以 UTF-8 發佈。當它從編碼不是 UTF-8 的頁面載入時,這會導致 錯誤,因為程式碼包含以下陳述
var
π
=
Math
.
PI
,
ε
=
1
e
-
6
;
識別碼 π 和 ε 未正確解碼,且未被辨認為有效的變數名稱。此外,某些超出 7 位元的編碼點的字串常數也未正確解碼。作為解決方法,您可以透過將適當的 charset
屬性新增到 <script>
標籤來載入程式碼
<script
charset=
"utf-8"
src=
"d3.js"
></script>
JavaScript 字串是 UTF-16 編碼單位的序列。根據 ECMAScript 規格,第 8.4 節
當字串包含實際文字資料時,每個元素會被視為單一 UTF-16 編碼單位。
如前所述,您可以在字串文字中 使用 Unicode 跳脫序列和十六進位跳脫序列。例如,您可以透過將 o 與分音符號 (代碼點 0x0308) 結合來產生 ö 字元:
> console.log('o\u0308') ö
這在 JavaScript 命令列中有效,例如網路瀏覽器主控台和 Node.js REPL。您也可以將此類字串插入網頁的 DOM 中。
網路上有許多 很棒的 Unicode 符號表。看看 Tim Whitlock 的 「Emoji Unicode 表格」,並驚嘆於現代 Unicode 字型中有多少符號。表格中的符號都不是圖片;它們都是字型字形。假設您想透過 JavaScript 顯示星體平面中的 Unicode 字元 (顯然,這樣做有風險:並非所有字型都支援所有此類字元)。例如,考慮一隻牛,代碼點 0x1F404: 。
您可以複製字元並直接貼到您的 Unicode 編碼 JavaScript 來源
JavaScript 引擎會解碼來源 (最常使用 UTF-8) 並建立一個包含兩個 UTF-16 編碼單位的字串。或者,您可以自己計算兩個編碼單位並使用 Unicode 跳脫序列。有網路應用程式可以執行此計算,例如:
先前定義的函式 toUTF16
也會執行此操作
> toUTF16(0x1F404) '\\uD83D\\uDC04'
UTF-16 代理對 (0xD83D, 0xDC04) 確實編碼了牛
如果字串 包含代理對 (兩個編碼單一碼點的碼元),則 length
屬性不再計算字形。它計算碼元:
這可以使用程式庫來修正,例如 Mathias Bynens 的 Punycode.js,它與 Node.js 捆綁在一起
> var puny = require('punycode'); > puny.ucs2.decode(str).length 1
如果您想要 在字串中搜尋或比較它們,則需要標準化—例如,透過程式庫 unorm (由 Bjarke Walling 所撰寫)。
JavaScript 正規表示式中 Unicode 的支援 (請參閱 第 19 章) 非常有限。例如,沒有辦法比對 Unicode 類別,例如「大寫字母」。
換行符號會影響比對。換行符號是下列表格中指定的四個字元之一:
碼元 | 名稱 | 字元跳脫序列 |
\u000A | 換行 |
|
\u000D | 回車 |
|
\u2028 | 行分隔符號 | |
\u2029 | 段落分隔符號 |
下列正規表示式結構是基於 Unicode
\s \S
(空白、非空白) 具有基於 Unicode 的定義
> /^\s$/.test('\uFEFF') true
.
(點) 比對所有碼元 (不是碼點!),但換行符號除外。請參閱下一節,以了解如何比對任何碼點。
/m
:在多行模式中,宣告 ^
比對輸入的開頭和換行符號之後。宣告 $
比對換行符號之前和輸入的結尾。在非多行模式中,它們分別只比對輸入的開頭或結尾。
其他重要的字元類別具有基於 ASCII 而不是 Unicode 的定義
\d \D
(數字、非數字):數字等於 [0-9]
。
\w \W
(字元、非字元):字元等於 [A-Za-z0-9_]
。
\b \B
(在字詞中斷、在字詞內):字詞是字元序列 ([A-Za-z0-9_]
)。例如,在字串 'über'
中,字元類別跳脫 \b
視字元 b 為字詞開頭
> /\bb/.test('über') true
若要比對任何 碼元,您可以使用 [\s\S]
;請參閱 原子:一般。
若要比對任何碼點,您需要使用:[20]
([\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])
前述模式運作方式如下
([BMP code point]|[leading surrogate][trailing surrogate])
由於這些範圍全部不相交,因此模式將正確比對結構良好的 UTF-16 字串中的碼點。
少數幾個函式庫有助於 處理 JavaScript 中的 Unicode:
XRegExp 是一個正規表示式函式庫,有一個 官方附加元件,可透過下列三種建構函式之一來比對 Unicode 類別、腳本、區塊和屬性
\p{...} \p{^...} \P{...}
例如,\p{Letter}
會比對各種字母表中的字母,而 \p{^Letter}
和 \P{Letter}
則會比對所有其他碼點。 第 30 章包含 XRegExp 的簡要概觀。
如需有關 Unicode 的更多資訊,請參閱 下列內容:
有關 JavaScript 中 Unicode 支援的資訊,請參閱
以下人員對此章節有貢獻:Mathias Bynens (@mathias)、Anne van Kesteren (@annevk) 和 Calvin Metcalf (@CWMma)。