首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

JavaScript如何正确处理Unicode编码问题

JavaScript 处理 Unicode 的方式至少可以说是令人惊讶的。本文解释了 JavaScript 中的 处理 Unicode 相关的痛点,提供了常见问题的解决方案,并解释了ECMAScript 6 标准如何改进这种情况。

Unicode 基础知识

在深入研究 JavaScript 之前,先解释一下 Unicode 一些基础知识,这样在 Unicode 方面,我们至少都了解一些。

Unicode是目前绝大多数程序使用的字符编码,定义也很简单,用一个?码位(code point)?映射一个字符。码位值的范围是从?U+0000?到?U+10FFFF?,可以表示超过 110 万个字符。下面是一些字符与它们的码位。

  • A 的码位 U+0041
  • a 的码位 U+0061
  • ? 的码位 U+00A9
  • ? 的码位 U+2603
  • :hankey: 的码位 U+1F4A9

码位通常被格式化为十六进制数字,零填充至少四位数,格式为?U +前缀?。

Unicode 最前面的 65536 个字符位,称为?基本多文种平面(BMP-—Basic Multilingual Plane)?,又简称为“?零号平面?”, plane 0),它的 码位 范围是从?U+0000?到?U+FFFF?。最常见的字符都放在这个平面上,这是 Unicode 最先定义和公布的一个平面。

剩下的字符都放在?辅助平面(Supplementary Plane)?或者?星形平面(astral planes)?,码位范围从?U+010000?一直到?U+10FFFF?,共 16 个辅助平面。

辅助平面内的码位很容易识别:如果需要超过 4 个十六进制数字来表示码位,那么它就是一个辅助平面内的码。

现在对 Unicode 有了基本的了解,接下来看看它如何应用于 JavaScript 字符串。

转义序列

在谷歌控制台输入如下:

代码语言:javascript
复制
>>?'\x41\x42\x43'?'ABC'??>>?'\x61\x62\x63'?'abc'?

以下称为十六进制转义序列。它们由引用匹配码位的两个十六进制数字组成。例如,?\x41?码位为?U+0041?表示大写字母 A。这些转义序列可用于?U+0000?到?U+00FF?范围内的码位。

同样常见的还有以下类型的转义:

代码语言:javascript
复制
>>?'\u0041\u0042\u0043'?'ABC'??>>?'I?\u2661?JavaScript!'?'I???JavaScript!'?

这些被称为?Unicode转义序列?。它们由表示码位的 4 个十六进制数字组成。例如,?\u2661?表示码位为?\U+2661?表示一个心。这些转义序列可以用于?U+0000?到?U+FFFF?范围内的码位,即整个基本平面。

但是其他的所有辅助平面呢? 我们需要 4 个以上的十六进制数字来表示它们的码位,那么如何转义它们呢?

在 ECMAScript 6中,这很简单,因为它引入了一种新的转义序列:?Unicode 码位转义?。例如:

代码语言:javascript
复制
>>?'\u{41}\u{42}\u{43}'?'ABC'??>>?'\u{1F4A9}'?':hankey:'?//?U+1F4A9?PILE?OF?POO?

在大括号之间可以使用最多 6 个十六进制数字,这足以表示所有 Unicode 码位。因此,通过使用这种类型的转义序列,可以基于其代码位轻松转义任何 Unicode 码位。

为了向后兼容 ECMAScript 5 和更旧的环境,不幸的解决方案是使用代理对:

代码语言:javascript
复制
>>?'\uD83D\uDCA9'?':hankey:'?//?U+1F4A9?PILE?OF?POO?

在这种情况下,每个转义表示代理项一半的码位。两个代理项就组成一个辅助码位。

注意,代理项对码位与原始码位全不同。?有公式?可以根据给定的辅助码位来计算代理项对码位,反之亦然——根据代理对计算原始辅助代码位。

辅助平面(Supplementary Planes)中的码位,在 UTF-16 中被编码为一对16 比特长的码元(即32bit,4Bytes),称作?代理对(surrogate pair)?,具体方法是:

  • 码位减去?0x10000?,得到的值的范围为 20 比特长的?0..0xFFFFF?.
  • 高位的 10 比特的值(值的范围为?0..0x3FF?)被加上?0xD800?得到第一个码元或称作?高位代理?。
  • 低位的 10 比特的值(值的范围也是?0..0x3FF?)被加上?0xDC00?得到第二个码元或称作?低位代理(low surrogate)?,现在值的范围是?0xDC00..0xDFFF?.

使用代理对,所有辅助平面中的码位(即从?U+010000?到?U+10FFFF?)都可以表示,但是使用一个转义来表示基本平面的码位,以及使用两个转义来表示辅助平面中的码位,整个概念是令人困惑的,并且会产生许多恼人的后果。

使用 JavaScript 字符串方法来计算字符长度

例如,假设你想要计算给定字符串中的字符个数。你会怎么做呢?

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

代码语言:javascript
复制
>>?'A'.length?//?码位:?U+0041?表示?A?1??>>?'A'?==?'\u0041'?true??>>?'B'.length?//?码位:?U+0042?表示?B?1??>>?'B'?==?'\u0042'?true?

在这些例子中,字符串的?length?属性恰好反映了字符的个数。这是有道理的:如果我们使用转义序列来表示字符,很明显,我们只需要对每个字符进行一次转义。但情况并非总是如此!这里有一个稍微不同的例子:

代码语言:javascript
复制
>>?'??'.length?//?码位:?U+1D400?表示?Math?Bold?字体大写?A?2??>>?'??'?==?'\uD835\uDC00'?true??>>?'??'.length?//?码位:?U+1D401?表示?Math?Bold?字体大写?B?2??>>?'??'?==?'\uD835\uDC01'?true??>>?':hankey:'.length?//?U+1F4A9?PILE?OF?POO?2??>>?':hankey:'?==?'\uD83D\uDCA9'?true?

在内部,JavaScript 将辅助平面内的字符表示为代理对,并将单独的代理对部分开为单独的 “字符”。如果仅使用 ECMAScript 5 兼容转义序列来表示字符,将看到每个辅助平面内的字符都需要两个转义。这是令人困惑的,因为人们通常用 Unicode 字符或图形来代替。

计算辅助平面内的字符个数

回到这个问题:如何准确地计算 JavaScript 字符串中的字符个数 ? 诀窍就是如何正确地解析代理对,并且只将每对代理对作为一个字符计数。你可以这样使用:

代码语言:javascript
复制
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?,利用它的实用方法在 JavaScript 字符串和 Unicode 码位之间进行转换。?decode?方法接受一个字符串并返回一个 Unicode 编码位数组;每个字符对应一项。

代码语言:javascript
复制
function?countSymbols(string)?{?????return?punycode.ucs2.decode(string).length;?}?

在 ES6 中,可以使用?Array.from?来做类似的事情,它使用字符串的迭代器将其拆分为一个字符串数组,每个字符串数组包含一个字符:

代码语言:javascript
复制
function?countSymbols(string)?{?????return?Array.from(string).length;?}?

或者,使用解构运算符?...?:

代码语言:javascript
复制
function?countSymbols(string)?{?????return?[...string].length;?}?

使用这些实现,我们现在可以正确地计算码位,这将导致更准确的结果:

代码语言:javascript
复制
>>?countSymbols('A')?//?码位:U+0041?表示?A?1??>>?countSymbols('??')?//?码位:?U+1D400?表示?Math?Bold?字体大写?A?1??>>?countSymbols(':hankey:')?//?U+1F4A9?PILE?OF?POO?1?

找撞脸

考虑一下这个例子:

代码语言:javascript
复制
>>?'ma?ana'?==?'man?ana'?false?

JavaScript告诉我们,这些字符串是不同的,但视觉上,没有办法告诉我们!这是怎么回事?

JavaScript转义工具?会告诉你,原因如下:

代码语言:javascript
复制
>>?'ma\xF1ana'?==?'man\u0303ana'?false??>>?'ma\xF1ana'.length?6??>>?'man\u0303ana'.length?7?

第一个字符串包含码位?U+00F1?表示字母 n 和 n 头上波浪号,而第二个字符串使用两个单独的码位(?U+006E?表示字母 n 和?U+0303?表示波浪号)来创建相同的字符。这就解释了为什么它们的长度不同。

然而,如果我们想用我们习惯的方式来计算这些字符串中的字符个数,我们希望这两个字符串的长度都为 6,因为这是每个字符串中可视可区分的字符的个数。要怎样才能做到这一点呢?

在ECMAScript 6 中,解决方案相当简单:

代码语言:javascript
复制
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?方法执行?Unicode规范?化,这解释了这些差异。 如果有一个码位表示与另一个码位后跟组合标记相同的字符,则会将其标准化为单个码位形式。

代码语言:javascript
复制
>>?countSymbolsPedantically('ma?ana')?//?U+00F1?6?>>?countSymbolsPedantically('man?ana')?//?U+006E?+?U+0303?6?

为了向后兼容 ECMAScript5 和旧环境,可以使用?String.prototype.normalize polyfill?。

计算其他组合标记

然而,上述方案仍然不是完美的——应用多个组合标记的码位总是导致单个可视字符,但可能没有 normalize 的形式,在这种情况下,normalize 是没有帮助。例如:

代码语言:javascript
复制
>>?'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?

如果需要更精确的解决方案,可以使用正则表达式从输入字符串中删除任何组合标记。

代码语言:javascript
复制
//??将下面的正则表达式替换为经过转换的等效表达式,以使其在旧环境中工作??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;?}?

此函数删除任何组合标记,只留下它们所属的字符。任何不匹配的组合标记(在字符串开头)都保持不变。这个解决方案甚至可以在 ECMAScript3 环境中工作,并且它提供了迄今为止最准确的结果:

代码语言:javascript
复制
>>?countSymbolsIgnoringCombiningMarks('q\u0307\u0323')?1?>>?countSymbolsIgnoringCombiningMarks('Z??????????????????A????????L?????G????????????O??????????!????????????????')?6?

计算其他类型的图形集群

上面的算法仍然是一个简化—它还是无法正确计算像这样的字符:??,汉语言由连体的 Jamo 组成,如 ???, 表情字符序列,如 :man:?:woman:?:girl:?:boy: ((:man:?U+200D?+ :woman:?U+200D?+ :girl: +?U+200D?+ :boy:)或其他类似字符。

Unicode 文本分段上的 Unicode 标准附件#29 描述了用于确定字形簇边界的算法。 对于适用于所有?Unicode脚本的完全准确的解决方案?,请在 JavaScript 中实现此算法,然后将每个字形集群计为单个字符。?有人建议将Intl.Segmenter(一种文本分段API)添加到ECMAScript中?。

JavaScript 中字符串反转

下面是一个类似问题的示例:在JavaScript中反转字符串。这能有多难,对吧? 解决这个问题的一个常见的、非常简单的方法是:

代码语言:javascript
复制
function?reverse(string)?{?????return?string.split('').reverse().join('');?}?

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

代码语言:javascript
复制
>>?reverse('abc')?'cba'??>>?reverse('ma?ana')?//?U+00F1?'ana?am'?

然而,它完全打乱了包含组合标记或位于辅助平面字符的字符串。

代码语言:javascript
复制
>>?reverse('ma?ana')?//?U+006E?+?U+0303?'ana?nam'?//?note:?the?`~`?is?now?applied?to?the?`a`?instead?of?the?`n`??>>?reverse(':hankey:')?//?U+1F4A9?'??'?//?`'\uDCA9\uD83D'`,?the?surrogate?pair?for?`:hankey:`?in?the?wrong?order?

要在 ES6 中正确反转位于辅助平面字符,字符串迭代器可以与?Array.from?结合使用:

代码语言:javascript
复制
function?reverse(string)?{???return?Array.from(string).reverse().join('');?}?

但是,这仍然不能解决组合标记的问题。

幸运的是,一位名叫 Missy Elliot 的聪明的计算机科学家提出了一个?防弹算法?来解释这些问题。它看上去像这样:

我把丁字裤放下,翻转,然后倒过来。我把丁字裤放下,翻转,然后倒过来。

事实上:通过将任何组合标记的位置与它们所属的字符交换,以及在进一步处理字符串之前反转任何代理对,可以成功避免问题。

代码语言:javascript
复制
//?使用库?Esrever?(https://mths.be/esrever)??>>?esrever.reverse('ma?ana')?//?U+006E?+?U+0303?'ana?am'??>>?esrever.reverse(':hankey:')?//?U+1F4A9?':hankey:'?//?U+1F4A9?

字符串方法中的 Unicode 的问题

这种行为也会影响其他字符串方法。

将码位转转换为字符

String.fromCharCode?可以将一个码位转换为字符。 但它只适用于?BMP?范围内的码位 ( 即从?U+0000?到?U+FFFF?)。如果将它用于转换超过?BMP?平面外的码位 ,将获得意想不到的结果。

代码语言:javascript
复制
>>?String.fromCharCode(0x0041)?//?U+0041?'A'?//?U+0041??>>?String.fromCharCode(0x1F4A9)?//?U+1F4A9?'?'?//?U+F4A9,?not?U+1F4A9?

唯一的解决方法是自己计算代理项一半的码位,并将它们作为单独的参数传递。

代码语言:javascript
复制
>>?String.fromCharCode(0xD83D,?0xDCA9)?':hankey:'?//?U+1F4A9?

如果不想计算代理项的一半,可以使用?Punycode.js?的实用方法:

代码语言:javascript
复制
>>?punycode.ucs2.encode([?0x1F4A9?])?':hankey:'?//?U+1F4A9?

幸运的是,ECMAScript 6 引入了?String.fromCodePoint(codePoint)?,它可以位于基本平面外的码位的字符。它可以用于任何 Unicode 编码点,即从?U+000000?到?U+10FFFF?。

代码语言:javascript
复制
>>?String.fromCodePoint(0x1F4A9)?':hankey:'?//?U+1F4A9?

为了向后兼容ECMAScript 5 和更旧的环境,使用?String.fromCodePoint() polyfill?。

从字符串中获取字符

如果使用?String.prototype.charAt(position)?来检索包含字符串中的第一个字符,则只能获得第一个代理项而不是整个字符。

代码语言:javascript
复制
>>?':hankey:'.charAt(0)?//?U+1F4A9?'\uD83D'?//?U+D83D,?i.e.?the?first?surrogate?half?for?U+1F4A9?

有人提议在 ECMAScript 7 中引入?String.prototype.at(position)?。它类似于?charAt?,只不过它尽可能地处理完整的字符而不是代理项的一半。

代码语言:javascript
复制
>>?':hankey:'.at(0)?//?U+1F4A9?':hankey:'?//?U+1F4A9?

为了向后兼容 ECMAScript 5 和更旧的环境,可以使用?String.prototype.at() polyfill/prollyfill。

从字符串中获取码位

类似地,如果使用?String.prototype.charCodeAt(position)?检索字符串中第一个字符的码位,将获得第一个代理项的码位,而不是 poo 字符堆的码位。

代码语言:javascript
复制
>>?':hankey:'.charCodeAt(0)?0xD83D?

幸运的是,ECMAScript 6 引入了?String.prototype.codePointAt(position)?,它类似于?charCodeAt?,只不过它尽可能处理完整的字符而不是代理项的一半。

代码语言:javascript
复制
>>?':hankey:'.codePointAt(0)?0x1F4A9?

为了向后兼容 ECMAScript 5 和更旧的环境,使用?String.prototype.codePointAt()_polyfill。

遍历字符串中的所有字符

假设想要循环字符串中的每个字符,并对每个单独的字符执行一些操作。

在 ECMAScript 5 中,你必须编写大量的样板代码来判断代理对

代码语言:javascript
复制
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(':hankey:');?symbols.forEach(function(symbol)?{?????console.log(symbol?==?':hankey:');?});?

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

在 ECMAScript 6中,你可以简单地使用?for…of?。字符串迭代器处理整个字符,而不是代理对。

代码语言:javascript
复制
for?(const?symbol?of?':hankey:')?{?????console.log(symbol?==?':hankey:');?}?

不幸的是,没有办法对它进行填充,因为?for…of?是一个语法级结构。

其他问题

此行为会影响几乎所有字符串方法,包括此处未明确提及的方法(如?String.prototype.substring?,?String.prototype.slice?等),因此在使用它们时要小心。

正则表达式中的 Unicode 问题

匹配码位和 Unicode 标量值

正则表达式中的点运算符(?.?)只匹配一个“字符”, 但是由于JavaScript将代理半部分公开为单独的 “字符”,所以它永远不会匹配位于辅助平面上的字符。

代码语言:javascript
复制
>>?/foo.bar/.test('foo:hankey:bar')?false?

让我们思考一下,我们可以使用什么正则表达式来匹配任何 Unicode字符? 什么好主意吗? 如下所示的,?.?这w个是不够的,因为它不匹配换行符或整个位于辅助平面上的字符。

代码语言:javascript
复制
>>?/^.$/.test(':hankey:')?false?

为了正确匹配换行符,我们可以使用?[\s\S]?来代替,但这仍然不能匹配整个位于辅助平面上的字符。

代码语言:javascript
复制
>>?/^[\s\S]$/.test(':hankey:')?false?

事实证明,匹配任何 Unicode 编码点的正则表达式一点也不简单:

代码语言:javascript
复制
>>?/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/.test(':hankey:')?//?wtf?true?

当然,你不希望手工编写这些正则表达式,更不用说调试它们了。为了生成像上面的一个正则表达式,可以使用了一个名为?Regenerate?的库,它可以根据码位或字符列表轻松地创建正则表达式:

代码语言:javascript
复制
>>?regenerate().addRange(0x0,?0x10FFFF).toString()?'[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]'?

从左到右,这个正则表达式匹配BMP字符、代理项对或单个代理项。

虽然在 JavaScript 字符串中技术上允许使用单独的代理,但是它们本身并不映射到任何字符,因此应该避免使用。术语?Unicode标量值?指除代理码位之外的所有码位。下面是一个正则表达式,它匹配任何 Unicode 标量值:

代码语言:javascript
复制
>>?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?作为构建脚本的一部分使用的,用于创建复杂的正则表达式,同时仍然保持生成这些表达式的脚本的可读性和易于维护。

ECMAScript 6 为正则表达式引入一个?u?标志,它会使用?.?操作符匹配整个码位,而不是代理项的一半。

代码语言:javascript
复制
>>?/foo.bar/.test('foo:hankey:bar')?false??>>?/foo.bar/u.test('foo:hankey:bar')?true?

注意?.?操作符仍然不会匹配换行符,设置?u?标志时,?.?操作符等效于以下向后兼容的正则表达式模式:

代码语言:javascript
复制
>>?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:hankey:bar')?true?

位于辅助平面码位上的字符

考虑到?/[a-c]/?匹配任何字符从 码位为?U+0061?的字母 a 到 码位为?U+0063?的字母 c,似乎/[:hankey:-:dizzy:]/ 会匹配码位?U+1F4A9?到码位?U+1F4AB?,然而事实并非如此:

代码语言:javascript
复制
>>?/[:hankey:-:dizzy:]/?SyntaxError:?Invalid?regular?expression:?Range?out?of?order?in?character?class?

发生这种情况的原因是,正则表达式等价于:

代码语言:javascript
复制
>>?/[\uD83D\uDCA9-\uD83D\uDCAB]/?SyntaxError:?Invalid?regular?expression:?Range?out?of?order?in?character?class?

事实证明,不像我们想的那样匹配码位?U+1F4A9?到码位?U+1F4AB?,而是匹配正则表达式:

  • U+D83D(高代理位)
  • 从?U+DCA9?到?U+D83D?的范围(无效,因为起始码位大于标记范围结束的码位)
  • U+DCAB(低代理位)
代码语言:javascript
复制
>>?/[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCA9')?//?match?U+1F4A9?true??>>?/[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4A9}')?//?match?U+1F4A9?true??>>?/[:hankey:-:dizzy:]/u.test(':hankey:')?//?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??>>?/[:hankey:-:dizzy:]/u.test(':muscle:')?//?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??>>?/[:hankey:-:dizzy:]/u.test(':dizzy:')?//?match?U+1F4AB?true?

遗憾的是,这个解决方案不能向后兼容 ECMAScript 5 和更旧的环境。如果这是一个问题,应该使用 Regenerate 生成 es5兼容的正则表达式,处理辅助平面范围内的字符:

代码语言:javascript
复制
>>?regenerate().addRange(':hankey:',?':dizzy:')?'\uD83D[\uDCA9-\uDCAB]'??>>?/^\uD83D[\uDCA9-\uDCAB]$/.test(':hankey:')?//?match?U+1F4A9?true??>>?/^\uD83D[\uDCA9-\uDCAB]$/.test(':muscle:')?//?match?U+1F4AA?true??>>?/^\uD83D[\uDCA9-\uDCAB]$/.test(':dizzy:')?//?match?U+1F4AB?true?

实战中的 bug 以及如何避免它们

这种行为会导致许多问题。例如,Twitter 每条 tweet 允许 140 个字符,而它们的后端并不介意它是什么类型的字符——是否为辅助平面内的字符。但由于JavaScript 计数在其网站上的某个时间点只是读出字符串的长度,而不考虑代理项对,因此不可能输入超过 70 个辅助平面内的字符。(这个bug已经修复。)

许多处理字符串的JavaScript库不能正确地解析辅助平面内的字符。

例如,Countable.js 它没有正确计算辅助平面内的字符。

Underscore.string?有一个?reverse?方法,它不处理组合标记或辅助平面内的字符。(改用?Missy Elliot?的算法)

它还错误地解码辅助平面内的字符的 HTML 数字实体,例如?&#x1F4A9;?。 许多其他 HTML 实体转换库也存在类似的问题。(在修复这些错误之前,请考虑使用?he?代替所有 HTML 编码/解码需求。)

  • 发表于:
  • 原文链接http://news.51cto.com/art/201901/590233.htm
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com