第 24 章 Unicode 與 JavaScript
目錄
購買書籍
(廣告,請勿封鎖)

第 24 章 Unicode 與 JavaScript

本章為Unicode 簡介,以及如何在 JavaScript 中處理 Unicode。

Unicode 歷史

Unicode 於1987 年由 Joe Becker(Xerox)、Lee Collins(Apple)和 Mark Davis(Apple)發起。其構想是建立一個通用字元集,因為當時存在許多不相容的純文字編碼標準:多種 8 位元 ASCII、大五碼(繁體中文)、GB 2312(簡體中文)等。在 Unicode 之前,並不存在多語言純文字標準,但有支援結合多種編碼的富文字系統(例如 Apple 的 WorldScript)。

第一份 Unicode 草案提案於 1988 年發布。此後持續進行相關工作,工作小組也擴大規模。Unicode Consortium於 1991 年 1 月 3 日成立

Unicode Consortium 是一家非營利公司,致力於開發、維護和推廣軟體國際化標準和資料,特別是 Unicode 標準 [...]

Unicode 1.0 標準的第一冊於 1991 年 10 月發布,第二冊於 1992 年 6 月發布。

重要的 Unicode 概念

字元的概念看似簡單,但其實包含許多面向。這也是 Unicode 如此複雜的原因。以下是重要的基本概念:

字元和字形
兩個術語意義非常相似。字元是數位實體,而字形是書面語言的原子單位(字母、印刷連字、中文字元、標點符號等)。程式設計師以字元思考,但使用者以字形思考。有時會使用多個字元來表示單一字形。例如,我們可以結合字元o和字元 ^(抑揚符)來產生單一字形 ô。
字形
這是顯示字形的具體方式。有時,相同的字形會因其脈絡或其他因素而有不同的顯示方式。例如,字形fi可以表示為字形f和字形i,並以連字字形連接,或不使用連字。
碼位
Unicode 使用稱為碼點的數字來表示它支援的字元。碼點的十六進位範圍為 0x0 到 0x10FFFF (17 次 16 位元)。
碼元
為了儲存或傳輸碼點,我們將它們編碼為碼元,即長度固定的資料片段。長度以位元為單位,並由編碼方案決定,Unicode 有好幾種編碼方案,例如 UTF-8 和 UTF-16。名稱中的數字表示碼元的長度(以位元為單位)。如果碼點太大,無法放入單一碼元,則必須將它拆分為多個碼元;也就是說,表示單一碼點所需的碼元數量可能會有所不同。
BOM(位元組順序標記)

如果碼元大於單一位元組,則位元組順序很重要。BOM 是文字開頭的單一偽字元(可能編碼為多個碼元),它表示碼元是大端序(最高位元組優先)還是小端序(最低位元組優先)。沒有 BOM 的文字預設為大端序。BOM 也表示所使用的編碼;UTF-8、UTF-16 等編碼的 BOM 不同。此外,如果網頁瀏覽器沒有其他關於文字編碼的資訊,BOM 可作為 Unicode 的標記。然而,由於以下幾個原因,BOM 並不常使用:

正規化
有時,同一個字形可以用多種方式表示。例如,字形 ö 可以表示為單一碼點,或表示為o後接一個組合字元 ¨(分音符號,雙點)。正規化是指將文字轉換為標準表示;等效的碼點和碼點序列全部轉換為同一個碼點(或碼點序列)。這對於文字處理很有用(例如,搜尋文字)。Unicode 指定了多種正規化方式。
字元屬性

Unicode 規格為每個字元指定了多種屬性,以下是其中一些:

  • 名稱。英文名稱,由大寫字母 A–Z、數字 0–9、連字符 (-) 和 <space> 組成。兩個範例

    • “λ” 的名稱為 “GREEK SMALL LETTER LAMBDA”。
    • “!” 的名稱為 “EXCLAMATION MARK”。
  • 一般類別。將字元分為字母、大寫字母、數字和標點符號等類別。
  • 年齡。字元是在哪個 Unicode 版本中引入的 (1.0、1.1.、2.0 等)?
  • 已棄用。是否不建議使用該字元?
  • 還有更多.

碼點

碼點 的範圍最初為 16 位元。在 Unicode 版本 2.0 (1996 年 7 月) 中,它已擴充:現在分為 17 個平面,編號為 0 到 16。每個平面包含 16 位元 (十六進位表示法:0x0000–0xFFFF)。因此,在以下十六進位範圍中,四個底層數字以外的數字包含平面的數字。

  • 平面 0,基本多文種平面 (BMP):0x0000–0xFFFF
  • 平面 1,補充多文種平面 (SMP):0x10000–0x1FFFF
  • 平面 2,補充表意文字平面 (SIP):0x20000–0x2FFFF
  • 平面 3–13,未指派
  • 平面 14,補充特殊用途平面 (SSP):0xE0000–0xEFFFF
  • 平面 15–16,補充私人使用區 (S PUA A/B):0x0F0000–0x10FFFF

平面 1–16 稱為補充平面星體平面

Unicode 編碼

UTF-32 (Unicode 轉換格式 32) 是一種具有 32 位元碼單位的格式。任何碼點都可以由一個碼單位編碼,這使其成為唯一的定長編碼;對於其他編碼,編碼一個點所需的單位數會有所不同。

UTF-16 是一種具有 16 位元碼單位的格式,需要一到兩個單位來表示一個碼點。BMP 碼點可以用單個碼單位表示。較高的碼點是 20 位元 (16 乘以 16 位元),減去 0x10000 (BMP 的範圍) 之後。這些位元編碼為兩個碼單位 (一個所謂的代理對):

前導代理
最高有效位元 10 位元:儲存在範圍 0xD800–0xDBFF 中。也稱為高代理碼單位
後隨代理
最低有效位元 10 位元:儲存在範圍 0xDC00–0xDFFF 中。也稱為低代理碼單位

下表 (改編自 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。

UTF-8 有 8 位元代碼單元。它在傳統 ASCII 編碼和 Unicode 之間建立了一座橋樑。ASCII 只有 128 個字元,其數字與前 128 個 Unicode 代碼點相同。UTF-8 向下相容,因為所有 ASCII 碼都是有效的代碼單元。換句話說,範圍在 0–127 之內的單一代碼單元會編碼同一個範圍內的單一代碼點。此類代碼單元會以其最高位元為 0 來標記。另一方面,如果最高位元為 1,則會接著更多單元,以提供較高代碼點的額外位元。這會導致以下編碼配置:

  • 0000–007F: 0xxxxxxx (7 位元,儲存在 1 個位元組中)
  • 0080–07FF: 110xxxxx, 10xxxxxx (5+6 位元 = 11 位元,儲存在 2 個位元組中)
  • 0800–FFFF: 1110xxxx, 10xxxxxx, 10xxxxxx (4+6+6 位元 = 16 位元,儲存在 3 個位元組中)
  • 10000–1FFFFF: 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx (3+6+6+6 位元 = 21 位元,儲存在 4 個位元組中)。最高代碼點為 10FFFF,因此 UTF-8 有些額外的空間。

如果最高位元不是 0,則 0 之前的 1 的數量表示序列中有多少個代碼單元。初始代碼單元之後的所有代碼單元都有位元前綴 10。因此,初始代碼單元和後續代碼單元的範圍是分開的,這有助於從編碼錯誤中復原。

UTF-8 已成為最受歡迎的 Unicode 格式。最初,它之所以受歡迎是因為它向下相容於 ASCII。後來,它獲得了廣泛且一致的支援,橫跨作業系統、程式設計環境和應用程式,因此聲勢看漲。

JavaScript 原始碼和 Unicode

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> 標籤載入原始檔時,它會如下確定編碼

  • 如果檔案以 BOM 開頭,則編碼是 UTF 變體,具體取決於使用的 BOM。
  • 否則,如果檔案是透過 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 標頭中)。這可確保 charsettype 的值不會衝突。
  • 否則,將使用文件編碼,其中包含 <script> 標籤。例如,這是 HTML5 文件的開頭,其中 <meta> 標籤宣告文件編碼為 UTF-8

    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
    ...

    強烈建議您始終指定編碼。如果您沒有指定,將使用特定語言環境的 預設編碼。換句話說,人們在不同國家會看到不同的檔案。只有最低的 7 位元在不同語言環境中相對穩定。

我的建議可總結如下

  • 對於您自己的應用程式,您可以使用 Unicode。但您必須將應用程式的 HTML 頁面編碼指定為 UTF-8。
  • 對於程式庫,發佈 ASCII(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, ε = 1e-6;

識別碼 π 和 ε 未正確解碼,且未被辨認為有效的變數名稱。此外,某些超出 7 位元的編碼點的字串常數也未正確解碼。作為解決方法,您可以透過將適當的 charset 屬性新增到 <script> 標籤來載入程式碼

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

JavaScript 字串和 Unicode

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

Unicode 標準化

如果您想要 在字串中搜尋或比較它們,則需要標準化—例如,透過程式庫 unorm (由 Bjarke Walling 所撰寫)。

JavaScript 正規表示式和 Unicode

JavaScript 正規表示式中 Unicode 的支援 (請參閱 第 19 章) 非常有限。例如,沒有辦法比對 Unicode 類別,例如「大寫字母」。

換行符號會影響比對。換行符號是下列表格中指定的四個字元之一:

碼元 名稱 字元跳脫序列

\u000A

換行

\n

\u000D

回車

\r

\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:

  • Regenerate 有助於產生範圍(例如前述範圍),以比對任何碼元。它預計用於建置工具的一部分,但也可以動態運作,用於嘗試各種情況。
  • XRegExp 是一個正規表示式函式庫,有一個 官方附加元件,可透過下列三種建構函式之一來比對 Unicode 類別、腳本、區塊和屬性

    \p{...} \p{^...} \P{...}

    例如,\p{Letter} 會比對各種字母表中的字母,而 \p{^Letter}\P{Letter} 則會比對所有其他碼點。 第 30 章包含 XRegExp 的簡要概觀。

  • ECMAScript 國際化 API(請參閱 ECMAScript 國際化 API)提供 Unicode 感知整理(字串的排序和搜尋)等功能。

如需有關 Unicode 的更多資訊,請參閱 下列內容:

有關 JavaScript 中 Unicode 支援的資訊,請參閱

致謝

以下人員對此章節有貢獻:Mathias Bynens (@mathias)、Anne van Kesteren ‏(@annevk) 和 Calvin Metcalf ‏(@CWMma)。



[20] 嚴格來說,任何 Unicode 標量值

下一頁:25. ECMAScript 5 的新增功能