JavaScript 如何正確處理 Unicode 編碼問題通殃!

JavaScript 處理 Unicode 的方式至少可以說是令人驚訝的夫偶。本文解釋了 JavaScript 中的 處理 Unicode 相關(guān)的痛點(diǎn),提供了常見問題的解決方案尸闸,并解釋了ECMAScript 6 標(biāo)準(zhǔn)如何改進(jìn)這種情況彻亲。

Unicode 基礎(chǔ)知識

在深入研究 JavaScript 之前孕锄,先解釋一下 Unicode 一些基礎(chǔ)知識,這樣在 Unicode 方面苞尝,我們至少都了解一些畸肆。

Unicode?是目前絕大多數(shù)程序使用的字符編碼,定義也很簡單宙址,用一個?碼位(code point)?映射一個字符轴脐。碼位值的范圍是從?U+0000?到?U+10FFFF,可以表示超過 110 萬個字符曼氛。下面是一些字符與它們的碼位豁辉。

A 的碼位 U+0041

a 的碼位 U+0061

? 的碼位 U+00A9

? 的碼位 U+2603

?? 的碼位 U+1F4A9

碼位?通常被格式化為十六進(jìn)制數(shù)字,零填充至少四位數(shù)舀患,格式為?U +前綴徽级。

Unicode 最前面的 65536 個字符位,稱為?基本多文種平面(BMP-—Basic Multilingual Plane)聊浅,又簡稱為“零號平面”, plane 0),它的 碼位 范圍是從?U+0000?到?U+FFFF餐抢。最常見的字符都放在這個平面上,這是 Unicode 最先定義和公布的一個平面低匙。

剩下的字符都放在?輔助平面(Supplementary Plane)或者?星形平面(astral planes)?旷痕,碼位范圍從U+010000?一直到?U+10FFFF,共 16 個輔助平面顽冶。

輔助平面內(nèi)的碼位很容易識別:如果需要超過 4 個十六進(jìn)制數(shù)字來表示碼位欺抗,那么它就是一個輔助平面內(nèi)的碼。

現(xiàn)在對 Unicode 有了基本的了解强重,接下來看看它如何應(yīng)用于 JavaScript 字符串绞呈。

轉(zhuǎn)義序列

在谷歌控制臺輸入如下:

>> '\x41\x42\x43'

'ABC'


>> '\x61\x62\x63'

'abc'

以下稱為十六進(jìn)制轉(zhuǎn)義序列。它們由引用匹配碼位的兩個十六進(jìn)制數(shù)字組成间景。例如佃声,\x41?碼位為U+0041?表示大寫字母 A。這些轉(zhuǎn)義序列可用于?U+0000?到?U+00FF?范圍內(nèi)的碼位倘要。

同樣常見的還有以下類型的轉(zhuǎn)義:

>> '\u0041\u0042\u0043'

'ABC'


>> 'I \u2661 JavaScript!'

'I ? JavaScript!'

這些被稱為?Unicode轉(zhuǎn)義序列圾亏。它們由表示碼位的 4 個十六進(jìn)制數(shù)字組成。例如封拧,\u2661?表示碼位為\U+2661?表示一個心志鹃。這些轉(zhuǎn)義序列可以用于?U+0000?到?U+FFFF?范圍內(nèi)的碼位,即整個基本平面泽西。

但是其他的所有輔助平面呢曹铃? 我們需要 4 個以上的十六進(jìn)制數(shù)字來表示它們的碼位,那么如何轉(zhuǎn)義它們呢?

在 ECMAScript 6中尝苇,這很簡單铛只,因?yàn)樗肓艘环N新的轉(zhuǎn)義序列:?Unicode 碼位轉(zhuǎn)義。例如:

>> '\u{41}\u{42}\u{43}'

'ABC'


>> '\u{1F4A9}'

'??' // U+1F4A9 PILE OF POO

在大括號之間可以使用最多 6 個十六進(jìn)制數(shù)字糠溜,這足以表示所有 Unicode 碼位淳玩。因此,通過使用這種類型的轉(zhuǎn)義序列非竿,可以基于其代碼位輕松轉(zhuǎn)義任何 Unicode 碼位蜕着。

為了向后兼容 ECMAScript 5 和更舊的環(huán)境,不幸的解決方案是使用代理對:

>> '\uD83D\uDCA9'

'??' // U+1F4A9 PILE OF POO

在這種情況下红柱,每個轉(zhuǎn)義表示代理項(xiàng)一半的碼位承匣。兩個代理項(xiàng)就組成一個輔助碼位。

注意锤悄,代理項(xiàng)對碼位與原始碼位全不同韧骗。有公式可以根據(jù)給定的輔助碼位來計算代理項(xiàng)對碼位,反之亦然——根據(jù)代理對計算原始輔助代碼位零聚。

輔助平面(Supplementary Planes)中的碼位袍暴,在 UTF-16 中被編碼為一對16 比特長的碼元(即32bit,4Bytes),稱作代理對(surrogate pair)隶症,具體方法是:

碼位減去?0x10000,得到的值的范圍為 20 比特長的?0..0xFFFFF.

高位的 10 比特的值(值的范圍為?0..0x3FF)被加上?0xD800?得到第一個碼元或稱作高位代理政模。

低位的 10 比特的值(值的范圍也是?0..0x3FF)被加上?0xDC00?得到第二個碼元或稱作低位代理(low surrogate),現(xiàn)在值的范圍是?0xDC00..0xDFFF.

使用代理對蚂会,所有輔助平面中的碼位(即從?U+010000?到?U+10FFFF?)都可以表示淋样,但是使用一個轉(zhuǎn)義來表示基本平面的碼位,以及使用兩個轉(zhuǎn)義來表示輔助平面中的碼位胁住,整個概念是令人困惑的趁猴,并且會產(chǎn)生許多惱人的后果。

使用 JavaScript 字符串方法來計算字符長度

例如措嵌,假設(shè)你想要計算給定字符串中的字符個數(shù)躲叼。你會怎么做呢?

首先想到可能是使用?length?屬性。

>> 'A'.length // 碼位: U+0041 表示 A

1


>> 'A' == '\u0041'

true


>> 'B'.length // 碼位: U+0042 表示 B

1


>> 'B' == '\u0042'

true

在這些例子中企巢,字符串的?length?屬性恰好反映了字符的個數(shù)枫慷。這是有道理的:如果我們使用轉(zhuǎn)義序列來表示字符,很明顯浪规,我們只需要對每個字符進(jìn)行一次轉(zhuǎn)義或听。但情況并非總是如此!這里有一個稍微不同的例子:

>> '??'.length // 碼位: U+1D400 表示 Math Bold 字體大寫 A

2


>> '??' == '\uD835\uDC00'

true


>> '??'.length // 碼位: U+1D401 表示 Math Bold 字體大寫 B

2


>> '??' == '\uD835\uDC01'

true


>> '??'.length // U+1F4A9 PILE OF POO

2


>> '??' == '\uD83D\uDCA9'

true

在內(nèi)部笋婿,JavaScript 將輔助平面內(nèi)的字符表示為代理對誉裆,并將單獨(dú)的代理對部分開為單獨(dú)的 “字符”。如果僅使用 ECMAScript 5 兼容轉(zhuǎn)義序列來表示字符缸濒,將看到每個輔助平面內(nèi)的字符都需要兩個轉(zhuǎn)義足丢。這是令人困惑的粱腻,因?yàn)槿藗兺ǔS?Unicode 字符或圖形來代替。

計算輔助平面內(nèi)的字符個數(shù)

回到這個問題:如何準(zhǔn)確地計算 JavaScript 字符串中的字符個數(shù) ? 訣竅就是如何正確地解析代理對斩跌,并且只將每對代理對作為一個字符計數(shù)绍些。你可以這樣使用:

var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;


function countSymbols(string) {

? ? return string

? ? ? ? // Replace every surrogate pair with a BMP symbol.

? ? ? ? .replace(regexAstralSymbols, '_')

? ? ? ? // …and *then* get the length.

? ? ? ? .length;

}

或者,如果你使用?Punycode.js耀鸦,利用它的實(shí)用方法在 JavaScript 字符串和 Unicode 碼位之間進(jìn)行轉(zhuǎn)換柬批。decode?方法接受一個字符串并返回一個 Unicode 編碼位數(shù)組;每個字符對應(yīng)一項(xiàng)。

function countSymbols(string) {

? ? return punycode.ucs2.decode(string).length;

}

在 ES6 中袖订,可以使用?Array.from?來做類似的事情氮帐,它使用字符串的迭代器將其拆分為一個字符串?dāng)?shù)組,每個字符串?dāng)?shù)組包含一個字符:

function countSymbols(string) {

? ? return Array.from(string).length;

}

或者洛姑,使用解構(gòu)運(yùn)算符?...?:

function countSymbols(string) {

? ? return [...string].length;

}

使用這些實(shí)現(xiàn)上沐,我們現(xiàn)在可以正確地計算碼位,這將導(dǎo)致更準(zhǔn)確的結(jié)果:

>> countSymbols('A') // 碼位:U+0041 表示 A

1


>> countSymbols('??') // 碼位: U+1D400 表示 Math Bold 字體大寫 A

1


>> countSymbols('??') // U+1F4A9 PILE OF POO

1

找撞臉

考慮一下這個例子:

>> 'ma?ana' == 'man?ana'

false

JavaScript告訴我們楞艾,這些字符串是不同的奄容,但視覺上,沒有辦法告訴我們产徊!這是怎么回事?


JavaScript轉(zhuǎn)義工具?會告訴你昂勒,原因如下:

>> 'ma\xF1ana' == 'man\u0303ana'

false


>> 'ma\xF1ana'.length

6


>> 'man\u0303ana'.length

7

第一個字符串包含碼位?U+00F1?表示字母 n 和 n 頭上波浪號,而第二個字符串使用兩個單獨(dú)的碼位(U+006E表示字母 n 和?U+0303?表示波浪號)來創(chuàng)建相同的字符舟铜。這就解釋了為什么它們的長度不同戈盈。

然而,如果我們想用我們習(xí)慣的方式來計算這些字符串中的字符個數(shù)谆刨,我們希望這兩個字符串的長度都為 6塘娶,因?yàn)檫@是每個字符串中可視可區(qū)分的字符的個數(shù)。要怎樣才能做到這一點(diǎn)呢?

在ECMAScript 6 中痊夭,解決方案相當(dāng)簡單:

function countSymbolsPedantically(string) {

? ? // Unicode Normalization, NFC form, to account for lookalikes:

? ? var normalized = string.normalize('NFC');

? ? // Account for astral symbols / surrogates, just like we did before:

? ? return punycode.ucs2.decode(normalized).length;

}

String.prototype?上的?normalize?方法執(zhí)行?Unicode規(guī)范化滔吠,這解釋了這些差異偷俭。 如果有一個碼位表示與另一個碼位后跟組合標(biāo)記相同的字符壳猜,則會將其標(biāo)準(zhǔn)化為單個碼位形式谷誓。

>> countSymbolsPedantically('ma?ana') // U+00F1

6

>> countSymbolsPedantically('man?ana') // U+006E + U+0303

6

為了向后兼容 ECMAScript5 和舊環(huán)境,可以使用?String.prototype.normalize polyfill番舆。

計算其他組合標(biāo)記

然而酝碳,上述方案仍然不是完美的——應(yīng)用多個組合標(biāo)記的碼位總是導(dǎo)致單個可視字符,但可能沒有 normalize 的形式恨狈,在這種情況下疏哗,normalize 是沒有幫助。例如:

>> 'q\u0307\u0323'.normalize('NFC') // `q??`

'q\u0307\u0323'


>> countSymbolsPedantically('q\u0307\u0323')

3 // not 1


>> countSymbolsPedantically('Z??????????????????A????????L?????G???????????O??????????!????????????????')

74 // not 6

如果需要更精確的解決方案禾怠,可以使用正則表達(dá)式從輸入字符串中刪除任何組合標(biāo)記返奉。

// ?將下面的正則表達(dá)式替換為經(jīng)過轉(zhuǎn)換的等效表達(dá)式贝搁,以使其在舊環(huán)境中工作


var regexSymbolWithCombiningMarks = /(\P{Mark})(\p{Mark}+)/gu;


function countSymbolsIgnoringCombiningMarks(string) {

? ? // 刪除任何組合字符,只留下它們所屬的字符:

? ? var stripped = string.replace(regexSymbolWithCombiningMarks, function($0, symbol, combiningMarks) {

? ? ? ? return symbol;

? ? });


? ? return punycode.ucs2.decode(stripped).length;

}

此函數(shù)刪除任何組合標(biāo)記芽偏,只留下它們所屬的字符徘公。任何不匹配的組合標(biāo)記(在字符串開頭)都保持不變。這個解決方案甚至可以在 ECMAScript3 環(huán)境中工作哮针,并且它提供了迄今為止最準(zhǔn)確的結(jié)果:

>> countSymbolsIgnoringCombiningMarks('q\u0307\u0323')

1

>> countSymbolsIgnoringCombiningMarks('Z??????????????????A????????L?????G???????????O??????????!????????????????')

6

計算其他類型的圖形集群

上面的算法仍然是一個簡化—它還是無法正確計算像這樣的字符:??,漢語言由連體的 Jamo 組成坦袍,如 ???十厢, 表情字符序列,如 ??????????? ((?? U+200D + ?? U+200D + ?? + U+200D + ??)或其他類似字符捂齐。


Unicode 文本分段上的 Unicode 標(biāo)準(zhǔn)附件#29 描述了用于確定字形簇邊界的算法蛮放。 對于適用于所有 Unicode腳本的完全準(zhǔn)確的解決方案,請在 JavaScript 中實(shí)現(xiàn)此算法奠宜,然后將每個字形集群計為單個字符包颁。 有人建議將Intl.Segmenter(一種文本分段API)添加到ECMAScript中。


JavaScript 中字符串反轉(zhuǎn)

下面是一個類似問題的示例:在JavaScript中反轉(zhuǎn)字符串压真。這能有多難娩嚼,對吧? 解決這個問題的一個常見的、非常簡單的方法是:

function reverse(string) {

? ? return string.split('').reverse().join('');

}

它似乎在很多情況下都很有效:

>> reverse('abc')

'cba'


>> reverse('ma?ana') // U+00F1

'ana?am'

然而滴肿,它完全打亂了包含組合標(biāo)記或位于輔助平面字符的字符串岳悟。

>> reverse('ma?ana') // U+006E + U+0303

'ana?nam' // note: the `~` is now applied to the `a` instead of the `n`


>> reverse('??') // U+1F4A9

'??' // `'\uDCA9\uD83D'`, the surrogate pair for `??` in the wrong order

要在 ES6 中正確反轉(zhuǎn)位于輔助平面字符,字符串迭代器可以與?Array.from?結(jié)合使用:

function reverse(string) {

? return Array.from(string).reverse().join('');

}

但是泼差,這仍然不能解決組合標(biāo)記的問題贵少。

幸運(yùn)的是,一位名叫 Missy Elliot 的聰明的計算機(jī)科學(xué)家提出了一個防彈算法來解釋這些問題堆缘。它看上去像這樣:

我把丁字褲放下滔灶,翻轉(zhuǎn),然后倒過來吼肥。我把丁字褲放下录平,翻轉(zhuǎn),然后倒過來缀皱。

事實(shí)上:通過將任何組合標(biāo)記的位置與它們所屬的字符交換萄涯,以及在進(jìn)一步處理字符串之前反轉(zhuǎn)任何代理對,可以成功避免問題唆鸡。

// 使用庫 Esrever (https://mths.be/esrever)


>> esrever.reverse('ma?ana') // U+006E + U+0303

'ana?am'


>> esrever.reverse('??') // U+1F4A9

'??' // U+1F4A9

字符串方法中的 Unicode 的問題

這種行為也會影響其他字符串方法涝影。

將碼位轉(zhuǎn)轉(zhuǎn)換為字符

String.fromCharCode?可以將一個碼位轉(zhuǎn)換為字符。 但它只適用于?BMP?范圍內(nèi)的碼位 ( 即從?U+0000到U+FFFF)争占。如果將它用于轉(zhuǎn)換超過?BMP?平面外的碼位 燃逻,將獲得意想不到的結(jié)果序目。

>> String.fromCharCode(0x0041) // U+0041

'A' // U+0041


>> String.fromCharCode(0x1F4A9) // U+1F4A9

'?' // U+F4A9, not U+1F4A9

唯一的解決方法是自己計算代理項(xiàng)一半的碼位,并將它們作為單獨(dú)的參數(shù)傳遞伯襟。

>> String.fromCharCode(0xD83D, 0xDCA9)

'??' // U+1F4A9

如果不想計算代理項(xiàng)的一半猿涨,可以使用?Punycode.js?的實(shí)用方法:

>> punycode.ucs2.encode([ 0x1F4A9 ])

'??' // U+1F4A9

幸運(yùn)的是,ECMAScript 6 引入了?String.fromCodePoint(codePoint)姆怪,它可以位于基本平面外的碼位的字符叛赚。它可以用于任何 Unicode 編碼點(diǎn),即從?U+000000?到?U+10FFFF稽揭。

>> String.fromCodePoint(0x1F4A9) '??' // U+1F4A9

為了向后兼容ECMAScript 5 和更舊的環(huán)境俺附,使用?String.fromCodePoint() polyfill。

從字符串中獲取字符

如果使用?String.prototype.charAt(position)?來檢索包含字符串中的第一個字符溪掀,則只能獲得第一個代理項(xiàng)而不是整個字符事镣。

>> '??'.charAt(0) // U+1F4A9

'\uD83D' // U+D83D, i.e. the first surrogate half for U+1F4A9

有人提議在 ECMAScript 7 中引入?String.prototype.at(position)。它類似于charAt揪胃,只不過它盡可能地處理完整的字符而不是代理項(xiàng)的一半璃哟。

>> '??'.at(0) // U+1F4A9

'??' // U+1F4A9

為了向后兼容 ECMAScript 5 和更舊的環(huán)境,可以使用?String.prototype.at() polyfill/prollyfill喊递。

從字符串中獲取碼位

類似地随闪,如果使用?String.prototype.charCodeAt(position)?檢索字符串中第一個字符的碼位,將獲得第一個代理項(xiàng)的碼位骚勘,而不是 poo 字符堆的碼位蕴掏。

>> '??'.charCodeAt(0)

0xD83D

幸運(yùn)的是,ECMAScript 6 引入了?String.prototype.codePointAt(position)调鲸,它類似于charCodeAt盛杰,只不過它盡可能處理完整的字符而不是代理項(xiàng)的一半。

>> '??'.codePointAt(0)

0x1F4A9

為了向后兼容 ECMAScript 5 和更舊的環(huán)境藐石,使用?String.prototype.codePointAt()_polyfill即供。

遍歷字符串中的所有字符

假設(shè)想要循環(huán)字符串中的每個字符,并對每個單獨(dú)的字符執(zhí)行一些操作于微。

在 ECMAScript 5 中逗嫡,你必須編寫大量的樣板代碼來判斷代理對:

function getSymbols(string) {

? ? var index = 0;

? ? var length = string.length;

? ? var output = [];

? ? for (; index < length - 1; ++index) {

? ? ? ? var charCode = string.charCodeAt(index);

? ? ? ? if (charCode >= 0xD800 && charCode <= 0xDBFF) {

? ? ? ? ? ? charCode = string.charCodeAt(index + 1);

? ? ? ? ? ? if (charCode >= 0xDC00 && charCode <= 0xDFFF) {

? ? ? ? ? ? ? ? output.push(string.slice(index, index + 2));

? ? ? ? ? ? ? ? ++index;

? ? ? ? ? ? ? ? continue;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? output.push(string.charAt(index));

? ? }

? ? output.push(string.charAt(index));

? ? return output;

}


var symbols = getSymbols('??');

symbols.forEach(function(symbol) {

? ? console.log(symbol == '??');

});

或者可以使用正則表達(dá)式,如?var regexCodePoint = /[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;?并迭代匹配

在 ECMAScript 6中株依,你可以簡單地使用?for…of驱证。字符串迭代器處理整個字符,而不是代理對恋腕。

for (const symbol of '??') {

? ? console.log(symbol == '??');

}

不幸的是抹锄,沒有辦法對它進(jìn)行填充,因?yàn)?for…of?是一個語法級結(jié)構(gòu)。

其他問題

此行為會影響幾乎所有字符串方法伙单,包括此處未明確提及的方法(如String.prototype.substring获高,String.prototype.slice?等),因此在使用它們時要小心吻育。

正則表達(dá)式中的 Unicode 問題

匹配碼位和 Unicode 標(biāo)量值

正則表達(dá)式中的點(diǎn)運(yùn)算符(.)只匹配一個“字符”念秧, 但是由于JavaScript將代理半部分公開為單獨(dú)的 “字符”,所以它永遠(yuǎn)不會匹配位于輔助平面上的字符布疼。

>> /foo.bar/.test('foo??bar')

false

讓我們思考一下摊趾,我們可以使用什么正則表達(dá)式來匹配任何 Unicode字符? 什么好主意嗎? 如下所示的,.這w個是不夠的,因?yàn)樗黄ヅ鋼Q行符或整個位于輔助平面上的字符游两。

>> /^.$/.test('??')

false

為了正確匹配換行符砾层,我們可以使用?[\s\S]?來代替,但這仍然不能匹配整個位于輔助平面上的字符器罐。

>> /^[\s\S]$/.test('??')

false

事實(shí)證明,匹配任何 Unicode 編碼點(diǎn)的正則表達(dá)式一點(diǎn)也不簡單:

>> /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/.test('??') // wtf

true

當(dāng)然渐行,你不希望手工編寫這些正則表達(dá)式轰坊,更不用說調(diào)試它們了。為了生成像上面的一個正則表達(dá)式祟印,可以使用了一個名為?Regenerate?的庫肴沫,它可以根據(jù)碼位或字符列表輕松地創(chuàng)建正則表達(dá)式:

>> regenerate().addRange(0x0, 0x10FFFF).toString()

'[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]'

從左到右,這個正則表達(dá)式匹配BMP字符蕴忆、代理項(xiàng)對或單個代理項(xiàng)颤芬。

雖然在 JavaScript 字符串中技術(shù)上允許使用單獨(dú)的代理,但是它們本身并不映射到任何字符套鹅,因此應(yīng)該避免使用站蝠。術(shù)語?Unicode標(biāo)量值?指除代理碼位之外的所有碼位。下面是一個正則表達(dá)式卓鹿,它匹配任何 Unicode 標(biāo)量值:

>> regenerate()

? ? ?.addRange(0x0, 0x10FFFF) ? ? // all Unicode code points

? ? ?.removeRange(0xD800, 0xDBFF) // minus high surrogates

? ? ?.removeRange(0xDC00, 0xDFFF) // minus low surrogates

? ? ?.toRegExp()

/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/

Regenerate?作為構(gòu)建腳本的一部分使用的菱魔,用于創(chuàng)建復(fù)雜的正則表達(dá)式,同時仍然保持生成這些表達(dá)式的腳本的可讀性和易于維護(hù)吟孙。

ECMAScript 6 為正則表達(dá)式引入一個?u?標(biāo)志澜倦,它會使用?.?操作符匹配整個碼位,而不是代理項(xiàng)的一半杰妓。

>> /foo.bar/.test('foo??bar')

false


>> /foo.bar/u.test('foo??bar')

true


注意?.?操作符仍然不會匹配換行符藻治,設(shè)置?u?標(biāo)志時,.?操作符等效于以下向后兼容的正則表達(dá)式模式:

>> regenerate()

? ? ?.addRange(0x0, 0x10FFFF) // all Unicode code points

? ? ?.remove( ?// minus `LineTerminator`s (https://ecma-international.org/ecma-262/5.1/#sec-7.3):

? ? ? ?0x000A, // Line Feed <LF>

? ? ? ?0x000D, // Carriage Return <CR>

? ? ? ?0x2028, // Line Separator <LS>

? ? ? ?0x2029 ?// Paragraph Separator <PS>

? ? ?)

? ? ?.toString();

'[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]'


>> /foo(?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])bar/u.test('foo??bar')

true

位于輔助平面碼位上的字符

考慮到?/[a-c]/?匹配任何字符從 碼位為?U+0061?的字母 a 到 碼位為?U+0063?的字母 c,似乎/[??-??]/ 會匹配碼位?U+1F4A9?到碼位?U+1F4AB巷挥,然而事實(shí)并非如此:

>> /[??-??]/

SyntaxError: Invalid regular expression: Range out of order in character class

發(fā)生這種情況的原因是桩卵,正則表達(dá)式等價于:

>> /[\uD83D\uDCA9-\uD83D\uDCAB]/ SyntaxError: Invalid regular expression: Range out of order in character class

事實(shí)證明,不像我們想的那樣匹配碼位?U+1F4A9?到碼位?U+1F4AB,而是匹配正則表達(dá)式:

U+D83D(高代理位)

從?U+DCA9?到?U+D83D?的范圍(無效吸占,因?yàn)槠鹗即a位大于標(biāo)記范圍結(jié)束的碼位)

U+DCAB(低代理位)

>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCA9') // match U+1F4A9

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4A9}') // match U+1F4A9

true


>> /[??-??]/u.test('??') // match U+1F4A9

true


>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCAA') // match U+1F4AA

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4AA}') // match U+1F4AA

true


>> /[??-??]/u.test('??') // match U+1F4AA

true


>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCAB') // match U+1F4AB

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4AB}') // match U+1F4AB

true


>> /[??-??]/u.test('??') // match U+1F4AB

true

遺憾的是晴叨,這個解決方案不能向后兼容 ECMAScript 5 和更舊的環(huán)境。如果這是一個問題矾屯,應(yīng)該使用 Regenerate 生成 es5兼容的正則表達(dá)式兼蕊,處理輔助平面范圍內(nèi)的字符:

>> regenerate().addRange('??', '??')

'\uD83D[\uDCA9-\uDCAB]'


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('??') // match U+1F4A9

true


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('??') // match U+1F4AA

true


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('??') // match U+1F4AB

true

實(shí)戰(zhàn)中的 bug 以及如何避免它們

這種行為會導(dǎo)致許多問題。例如件蚕,Twitter 每條 tweet 允許 140 個字符孙技,而它們的后端并不介意它是什么類型的字符——是否為輔助平面內(nèi)的字符。但由于JavaScript 計數(shù)在其網(wǎng)站上的某個時間點(diǎn)只是讀出字符串的長度排作,而不考慮代理項(xiàng)對牵啦,因此不可能輸入超過 70 個輔助平面內(nèi)的字符。(這個bug已經(jīng)修復(fù)妄痪。)

許多處理字符串的JavaScript庫不能正確地解析輔助平面內(nèi)的字符哈雏。

例如,Countable.js 它沒有正確計算輔助平面內(nèi)的字符衫生。

Underscore.string?有一個?reverse?方法裳瘪,它不處理組合標(biāo)記或輔助平面內(nèi)的字符。(改用?Missy Elliot的算法)

它還錯誤地解碼輔助平面內(nèi)的字符的 HTML 數(shù)字實(shí)體罪针,例如?&#x1F4A9;彭羹。 許多其他 HTML 實(shí)體轉(zhuǎn)換庫也存在類似的問題。(在修復(fù)這些錯誤之前泪酱,請考慮使用?he?代替所有 HTML 編碼/解碼需求派殷。)


需要上圖完整資料或者更多Java全套視頻的可以添加裙;712352051 小編不看大家技術(shù)怎么樣 來的都?xì)g迎墓阀,包括小編最近整理的一套資料都可以分享給大家的
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毡惜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子斯撮,更是在濱河造成了極大的恐慌虱黄,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吮成,死亡現(xiàn)場離奇詭異橱乱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)粱甫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門泳叠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茶宵,你說我怎么就攤上這事危纫。” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵种蝶,是天一觀的道長契耿。 經(jīng)常有香客問我,道長螃征,這世上最難降的妖魔是什么搪桂? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮盯滚,結(jié)果婚禮上踢械,老公的妹妹穿的比我還像新娘。我一直安慰自己魄藕,他們只是感情好内列,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著背率,像睡著了一般话瞧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寝姿,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天交排,我揣著相機(jī)與錄音,去河邊找鬼会油。 笑死个粱,一個胖子當(dāng)著我的面吹牛古毛,可吹牛的內(nèi)容都是我干的翻翩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼稻薇,長吁一口氣:“原來是場噩夢啊……” “哼嫂冻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起塞椎,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桨仿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后案狠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體服傍,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年骂铁,在試婚紗的時候發(fā)現(xiàn)自己被綠了吹零。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡拉庵,死狀恐怖灿椅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤茫蛹,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布操刀,位于F島的核電站,受9級特大地震影響婴洼,放射性物質(zhì)發(fā)生泄漏骨坑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一窃蹋、第九天 我趴在偏房一處隱蔽的房頂上張望卡啰。 院中可真熱鬧,春花似錦警没、人聲如沸匈辱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亡脸。三九已至,卻和暖如春树酪,著一層夾襖步出監(jiān)牢的瞬間浅碾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工续语, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留垂谢,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓疮茄,卻偏偏與公主長得像滥朱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子力试,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容