JS中的字符串類型是由引號括起來的一組由16位Unicode字符組成的字符序列。在過去,16位足以包含任何字符,直到Unicode引入了擴展字符集,編碼規則不得不進行變動。本文將詳細介紹ES6關於Unicode的相關擴展正則表達式
Unicode的目標是爲世界上每個字符提供惟一標識符,惟一標識符稱爲碼位或碼點(code point)。而這些碼位是用於表示字符的,又稱爲字符編碼(character encode) 安全
在ES6以前, JS 的字符串以 16 位字符編碼(UTF-16)爲基礎。每一個 16 位序列(至關於2個字節)是一個編碼單元(code unit),可簡稱爲碼元,用於表示一個字符。字符串全部的屬性與方法(如length屬性與charAt() 方法等)都是基於16位序列函數
【BMP】編碼
最經常使用的Unicode字符使用16位序列編碼字符,屬於「基本多語種平面」(Basic Multilingual Plane BMP),也稱爲「零斷面」(plan 0), 是Unicode中的一個編碼區段,編碼介於U+0000——U+FFFF之間。超過這個範圍的碼位則要歸屬於某個輔助平面或稱爲擴展平面(supplementary plane),其中的碼位僅用16位就沒法表示了spa
爲此,UTF-16引入了代理對(surrogate pairs),規定用兩個16位編碼來表示一個碼位。這意味着,字符串裏的字符有兩種:一種由一個碼元(共 16 位)來表示BMP字符,另外一種用兩個碼元(共 32 位)來表示輔助平面字符代理
JavaScript 容許採用\uxxxx
形式表示一個字符,其中xxxx
表示字符的 Unicode 碼位code
// "a" console.log("\u0061");
可是,這種表示法只限於碼位在\u0000
~\uFFFF
之間的字符。超出這個範圍的字符,必須用兩個雙字節的形式表示orm
// "𠮷" console.log("\uD842\uDFB7"); // "₻7" console.log("\u20BB7");
上面代碼表示,若是直接在\u
後面跟上超過0xFFFF
的數值(好比\u20BB7
),JavaScript會理解成\u20BB+7
。因此會顯示一個特殊字符,後面跟着一個7
blog
ES6 對這一點作出了改進,只要將碼位放入大括號,就能正確解讀該字符ip
// "𠮷" console.log("\u{20BB7}"); // "ABC" console.log("\u{41}\u{42}\u{43}"); let hello = 123; // 123 console.log(hell\u{6F}); // true console.log('\u{1F680}' === '\uD83D\uDE80');
上面代碼中,最後一個例子代表,大括號表示法與四字節的 UTF-16 編碼是等價的。
有了這種表示法以後,JavaScript 共有6種方法能夠表示一個字符
'\z' === 'z' // true '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true '\u{7A}' === 'z' // true
【codePointAt()】
ES6新增了徹底支持UTF-16的方法codePointAt(),該方法接受編碼單元的位置而非字符位置做爲參數,返回與字符串中給定位置對應的碼位,即一個整數值
var text = "𠮷a" ; console.log(text.charCodeAt(0)); // 55362 console.log(text.charCodeAt(1)); // 57271 console.log(text.charCodeAt(2)); // 97 console.log(text.codePointAt(0)); // 134071 console.log(text.codePointAt(1)); // 57271 console.log(text.codePointAt(2)); // 97
對於BMP字符,codePointAt()方法的返回值與 charCodeAt() 相同,如'a',都返回97
對於輔助平面的32位字符,如'𠮷',charCodeAt()和codePointAt()方法都分爲兩部分返回
charCodeAt(0)和chatCodeAt(1)分別返回前16位和後16位的編碼;而codePointAt(0)和codePointAt(1)分別返回32位編碼及後16位的編碼
判斷一個字符是不是BMP,對該字符調用 codePointAt() 方法就是最簡單的方法
function is32Bit(c) { return c.codePointAt(0) > 0xFFFF; } console.log(is32Bit("𠮷" )); // true console.log(is32Bit("a")); // false
16位字符的上邊界用十六進制表示就是FFFF ,所以任何大於該數字的碼位必須用兩個碼元(共32位)表示
【String.fromCodePoint()】
ES5提供的String.fromCharCode
方法,用於從碼位返回對應字符,可是這個方法不能識別32位的UTF-16字符
ECMAScript一般會提供正反兩種方法。可使用codePointAt() 來提取字符串內中某個字符的碼位,也能夠藉助String.fromCodePoint()根據給定的碼位來生成一個字符
console.log(String.fromCharCode(0x20bb7)); // "ஷ" console.log(String.fromCodePoint(0x20bb7)); // "𠮷" console.log(String.fromCharCode(0x0bb7)); // "ஷ"
上面代碼中,String.fromCharCode
不能識別大於0xFFFF
的碼位,因此0x20BB7
就發生了溢出,最高位2
被捨棄了,最後返回碼位U+0BB7
對應的字符,而不是碼位U+20BB7
對應的字符
若是String.fromCodePoint()
方法有多個參數,則它們會被合併成一個字符串返回
// true String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
能夠將 String.fromCodePoint() 視爲 String.fromCharCode() 的完善版本。二者處理 BMP 字符時會返回相同結果,只有處理 BMP 範圍以外的字符時纔會有差別
對於32位的輔助平面字符來講,使用for或for in循環,可能得不到正確的結果
var s = '𠮷a'; for (let ch in s) { console.log(s[ch]); } //� //� //a
而for...of循環能夠正確的識別32位的UTF-16字符
var s = '𠮷a'; for (let ch of s) { console.log(ch); } //𠮷 //a
許多歐洲語言有語調符號和重音符號。爲了表示它們,Unicode提供了兩種方法。一種是直接提供帶重音符號的字符,好比Ǒ
(\u01D1)。另外一種是提供合成符號(combining character),即原字符與重音符號的合成,兩個字符合成一個字符,好比O
(\u004F)和ˇ
(\u030C)合成Ǒ
(\u004F\u030C)
這兩種表示方法,在視覺和語義上都等價,可是JavaScript不能識別
console.log('\u01D1'==='\u004F\u030C'); //false console.log('\u01D1'.length); // 1 console.log('\u004F\u030C'.length); // 2
上面代碼表示,JavaScript將合成字符視爲兩個字符,致使兩種表示方法不相等。
ES6提供字符串實例的normalize()
方法,用來將字符的不一樣表示方法統一爲一樣的形式,這稱爲Unicode正規化
console.log('\u01D1'==='\u01D1'.normalize()); //true console.log('\u01D1'=== '\u004F\u030C'.normalize()); //true
normalize
方法能夠接受一個參數來指定normalize
的方式,參數的四個可選值以下
一、NFC,默認參數,表示「標準等價合成」(Normalization Form Canonical Composition),返回多個簡單字符的合成字符。所謂「標準等價」指的是視覺和語義上的等價
console.log('\u01D1'==='\u01D1'.normalize("NFC")); //true console.log('\u01D1'=== '\u004F\u030C'.normalize("NFC")); //true
二、NFD,表示「標準等價分解」(Normalization Form Canonical Decomposition),即在標準等價的前提下,返回合成字符分解的多個簡單字符
console.log('\u004F\u030C'==='\u01D1'.normalize("NFD")); //true console.log('\u004F\u030C'=== '\u004F\u030C'.normalize("NFD")); //true
三、NFKC,表示「兼容等價合成」(Normalization Form Compatibility Composition),返回合成字符。所謂「兼容等價」指的是語義上存在等價,但視覺上不等價,好比「囍」和「喜喜」。(這只是用來舉例,normalize方法不能識別中文。)
四、NFKD,表示「兼容等價分解」(Normalization Form Compatibility Decomposition),即在兼容等價的前提下,返回合成字符分解的多個簡單字符
在開發國際化應用時,normalize() 方法很是有用。但normalize()
方法目前不能識別三個或三個以上字符的合成。這種狀況下,仍是隻能使用正則表達式,經過Unicode編號區間判斷
正則表達式能夠完成簡單的字符串操做,但默認將字符串中的每個字符按照16位編碼處理。爲了解決這個問題, ES6 對正則表達式添加了u
修飾符,含義爲「Unicode模式」,用來正確處理大於\uFFFF
的 Unicode 字符。也就是說,會正確處理四個字節的 UTF-16 編碼
/^\uD83D/u.test('\uD83D\uDC2A') // false /^\uD83D/.test('\uD83D\uDC2A') // true
一旦爲正則表達式設置了 u 修飾符,正則表達式將會識別32位的輔助平面字符爲1個字符,而不是兩個
【點號】
點(.
)字符在正則表達式中,含義是除了換行符之外的任意單個字符。對於碼位大於0xFFFF
的 Unicode 字符,點字符不能識別,必須加上u
修飾符
var text = "𠮷" ; console.log(text.length); // 2 console.log(/^.$/.test(text));//false console.log(/^.$/u.test(text)); //true
【大括號】
ES6 新增了使用大括號表示 Unicode 字符,這種表示法在正則表達式中必須加上u
修飾符,才能識別當中的大括號,不然會被解讀爲量詞
/\u{61}/.test('a') // false /\u{61}/u.test('a') // true /\u{20BB7}/u.test('𠮷') // true
【量詞】
使用u
修飾符後,全部量詞都會正確識別碼點大於0xFFFF
的 Unicode 字符
/a{2}/.test('aa') // true /a{2}/u.test('aa') // true /𠮷{2}/.test('𠮷𠮷') // false /𠮷{2}/u.test('𠮷𠮷') // true
【預約義模式】
u
修飾符也影響到預約義模式,可否正確識別碼點大於0xFFFF
的 Unicode 字符
/^\S$/.test('𠮷') // false /^\S$/u.test('𠮷') // true
【字符串長度】
上面代碼的\S
是預約義模式,匹配全部不是空格的字符。只有加了u
修飾符,它才能正確匹配碼點大於0xFFFF
的 Unicode 字符
雖然ES6不支持字符串碼位數量的檢測,length屬性仍然返回字符串編碼單元的數量。利用[\s\S],再加上u修飾符,就能夠寫出一個正確返回字符串長度的函數
function codePointLength(text) { var result = text.match(/[\s\S]/gu); return result ? result.length : 0; } var s = '𠮷𠮷'; console.log(s.length); // 4 console.log(codePointLength(s)); // 2
【檢測支持】
u修飾符是語法層面的變動,嘗試在不兼容 ES6 的 JS 引擎中使用它會拋出語法錯誤。若是要檢測當前引擎是否支持u修飾符,最安全的方式是經過如下函數來判斷
function hasRegExpU() { try { var pattern = new RegExp(".", "u"); return true; } catch (ex) { return false; } }
這個函數使用了RegExp構造函數並傳入字符串'u'做爲參數,該語法即便在舊版 JS 引擎中也是有效的。可是,若是當前引擎不支持u修飾符則會拋出錯誤