JavaScript 與 Unicode 規則表示式

July 11, 2022

JavaScript 沒有字元型態,撰寫程式碼時,以單引號或雙引號來包括單一 Unicode 字元,也是字串型態。JavaScript 在 1995 創建之時,UTF-16 尚未公佈,只能採用 UCS-2,使用兩個位元組為當時字元集的字元編碼,後來支援 UTF-16,以便能處理 U+0000 至 U+FFFF 以外的字元。

字串的元素

為了解決輸入法無法直接鍵入這類字元的問題,JavaScript 以字元在 UTF-16 編碼時的高低位元組來表示,也就是使用兩個碼元,例如高音譜記號的 Unicode 碼點為 U+1D11E,無法直接使用既有的 \uhhhh 來表示,在 ES5 或早期版本,字串必須撰寫為 '\uD834\uDD1E' 來表示,這稱為稱作代理對(Surrogate pair)。

在 ES6,增加了 \u{…} 表示法,可以直接撰寫 '\u{1D11E}',以碼點來表示高音譜記號,現今兩種表示法可以並存,例如:

> '\uD834\uDD1E' === '\u{1D11E}'
true
>

必須留意的是,因為 JavaScript 最初採用 UCS-2,對於原本就存在的 API 或索引,在處理字串時是以碼元為處理單位,支援 UTF-16 後為了兼顧相容性,ECMAScript 規定使用 UTF-16 碼元作為字串的元素(Element)單位 ,而不是把 Unicode 字元作為字串的一個元素。

Unicode 模式

從 ES6 開始,規則表示式啟用 u 旗標,代表著啟用 Unicode 模式,目的之一是支援 \u{…} 的寫法;例如,ES6 以後 '\uD834\uDD1E' 可以使用 '\u{1D11E}' 來表示;然而,若要在規則表示式使用 \u{…},必須開啟 u 旗標:

> 'Treble clef: \uD834\uDD1E'.search(/\u{1D11E}/)
-1
> 'Treble clef: \uD834\uDD1E'.search(/\u{1D11E}/u)
13
> 

在啟用 Unicode 模式的情況下,既有的 \uhhhh 寫法就是指定「碼點」(而不是碼元),也就是說,若 \uhhhh 指定的碼點處,實際上沒有定義 Unicode 字元,就不會比對成功。例如:

> let trebleClef = '\u{1D11E}'
undefined
> /\uD834/.test(trebleClef)
true
> /\uD834/u.test(trebleClef)
false
>

在沒有開啟 u 旗標的情況下,test 方法會在字串索引 0 處找到相符的碼元;然而,在開啟 Unicode 模式後,\uD834 就是指 Unicode 碼點 U+D834,然而該碼點處未定義字元,test 就因搜尋失敗而傳回 false

啟用 Unicode 模式後,對於 0xFFFF 以外的字元,才會進行正確的辨識,這會影響預定義字元類、量詞等的判斷。

例如,未啟用 Unicode 模式前,預定義字元類 \S 表示非空白字元,然而,對 0xFFFF 以外的字元會誤判,只有在加上 u 旗標的情況下才會正確比對;類似地,\W、「.」也只有在啟用 Unicode 模式下,才能正確比對 0xFFFF 以外的字元:

> /^\S$/.test('\u{1D11E}')
false
> /^\S$/u.test('\u{1D11E}')
true
> /^\W$/.test('\u{1D11E}')
false
> /^\W$/u.test('\u{1D11E}')
true
> /^.$/.test('\u{1D11E}')
false
> /^.$/u.test('\u{1D11E}')
true
>

Unicode 特性轉譯

ES9 以後支援規則表示式的 Unicode 特性轉譯(Unicode property escapes),為了能運用這個新功能,必須認識 Unicode 規範中的分類(Category)、文字(Script)。

在 Unicode 的規範中,每個 Unicode 字元會隸屬於某個分類,在〈General Category Property〉中可以看到 Letter、Uppercase Letter 等一般分類,每個分類也給予了 L、Lu 等縮寫名稱。

舉例來說,隸屬於 Letter 分類的字元都是字母,a 到 z、A 到 Z、全形的 a 到 z、A 到 Z 都在 Letter 分類中,除了英文字母之外,其他如希臘字母 α、β、γ 等,也都隸屬於 Letter 分類。

ES9 以後若使用 u 旗標開啟 Unicode 模式,可以使用 \p{General_Category=Letter}\p{gc=Letter}\p{Letter}\p{L} 等方式來指定分類,若分類名稱有兩個字,要使用 _ 相連,這樣在使用規則表示式判斷字母、數字等,就非常方便了。例如:

> /\p{General_Category=Letter}/u.test('α')
true
> /\p{gc=Letter}/u.test('α')
true
> /\p{Uppercase_Letter}/u.test('α')
false
> /\p{Ll}/u.test('α')
true
> /\p{Number}/u.test('1')
true
> /\p{Number}/u.test('1')
true

\p{…} 的相反就是 \P{…}

> /\p{Number}/u.test('1')
true
> /\P{Number}/u.test('1')
false
>

來個有趣的測試吧!𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼 都是十進位數字:

> /^\p{Decimal_Number}+$/u.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼')
true
>

數字呢?²³¹¼½¾𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ都是:

> /^\p{Number}+$/u.test('²³¹¼½¾𝟏𝟐𝟑𝟜𝟏𝟐𝟑𝟜���𝟪𝟫𝟬𝟭𝟮㉛㉛㉜㉜㉝㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ')
true
>

Unicode 將希臘文、漢字等組織為文字(Script)特性,可參考〈UNICODE SCRIPT PROPERTY〉,例如,如果想在規則表示式中以文字特性比對,可以使用 \p{Script=Greek}\p{Script_Extensions=Greek}\p{sc=Han}\p{scx=Han} 的寫法(Han 包含了正體中文、簡體中文,以及日、韓、越南文的全部漢字)。例如:

> /\p{Script=Greek}/u.test('a')
false
> /\p{Script_Extensions=Greek}/u.test('α')
true
> /\p{sc=Greek}/u.test('α')
true
> /\p{sc=Han}/u.test('林')
true
>

另外還有一些二元特性,像是 ASCII、Alphabetic、Lowercase、Emoji 等,直接寫在 \p{..} 裏就可以了。例如:

> /\p{Emoji}/u.test('😃')
true
>

如果想取得 \p{..} 中可以使用的特性名稱等清單,可以查閱 ECMAScript 規格書〈UnicodeMatchProperty〉與〈UnicodeMatchPropertyValue〉內容。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter