JavaScript 與 Unicode 規則表示式
July 11, 2022JavaScript 沒有字元型態,撰寫程式碼時,以單引號或雙引號來包括單一 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〉內容。