乐趣区

关于javascript:每个JavaScript开发人员都应该了解Unicode

这个故事以一个自白开始:我在很长一段时间都胆怯 Unicode。每当一个编程工作须要 Unicode 常识时,我正在寻找一个可破解的解决方案,而没有具体理解我在做什么。

我的回避始终继续到我遇到了一个须要具体 Unicode 常识的问题,再也没法回避。

通过一些致力,读了一堆文章——令人诧异的是,了解它并不难。嗯……有些文章至多须要读 3 遍。

事实证明,Unicode 是一种通用而优雅的规范。这可能很难,因为有一大堆难以保持的形象术语。

如果您在了解 Unicode 方面有差距,那么当初正是面对它的时候!没那么难。给本人沏杯美味的茶或咖啡☕. 让咱们深刻到形象微妙的世界。

这篇文章解释了 Unicode 的基本概念。这就发明了必要的根底。

而后说明 JavaScript 如何与 Unicode 协同工作,以及可能遇到的陷阱。

您还将学习如何利用新的 ECMAScript 2015 个性来解决局部艰难。

筹备好了吗?让咱们嗨起来!

1. Unicode 背地的理念

让咱们从一个根本问题开始。你如何浏览和了解以后的文章?简略地说:因为你晓得字母和单词作为一组字母的含意。

为什么你能了解字母的意思?简略地说:因为你(读者)和我(作者)在图形符号(屏幕上看到的货色)和英语字母(意思)之间的关联上达成了统一。

计算机也是如此。不同之处在于计算机不了解字母的含意:它们认为这些只是一些字节。

构想一个场景,当用户 1 通过网络向用户 2 发送音讯“hello”。

用户 1 的计算机不晓得字母的含意。因而,它将“hello”转换为一个数字序列0x68 0x65 0x6C 0x6C 0x6F,其中每个字母惟一地对应一个数字:h 是0x68,e 是0x65,等等。这些数字被发送到用户 2 的计算机。

当用户 2 的计算机接管到数字序列 0x68 0x65 0x6C 0x6C 0x6F 时,它将应用数字对应的字母并还原音讯。而后它会显示正确的音讯:“hello”。

这两台计算机之间对于字母和数字之间对应关系的协定是 Unicode 标准化的。

根据 Unicode,h 是一个名为 拉丁小写字母 h 的形象字符。该字符具备相应的数字0x68,代码点示意为U+0068

Unicode 的作用是提供一个形象字符列表(字符集),并为每个字符调配一个惟一的标识符代码点(编码字符集)。

2.Unicode 的根本术语

www.unicode.org 提到:“Unicode 为每个字符提供惟一的数字,”,与平台、编程和语言无关。

Unicode 是一种通用字符集,用于定义大多数书写零碎中的字符列表,并为每个字符关联一个惟一的数字(代码点)。

Unicode 包含来自当今大多数语言的字符、标点符号、变音符号、数学符号、技术符号、箭头、表情符号等等。

第一个 Unicode 版本 1.0 于 1991 年 10 月公布,共有 7161 个字符。最新版本 9.0(于 2016 年 6 月公布)提供了 128172 个字符的代码。

Unicode 的通用性和包容性解决了以前存在的一个次要问题,过后供应商实现了许多难以解决的字符集和编码。

创立一个反对所有字符集和编码的应用程序非常复杂。

如果您认为 Unicode 很难,那么没有 Unicode 的编程将更加艰难。

我依然记得我读着文件内容中的乱码,就跟买彩票一样!

2.1 字符和代码点

“形象字符(或字符)是用于组织、管制或示意文本数据的信息单元。”

Unicode 将字符作为形象术语解决。每个形象字符都有一个相干的名称,例如拉丁字母(LATIN SMALL LETTERA。该字符的出现模式(字形)为a

“代码点是一个调配给单个字符的数字。”

代码点的范畴是 U+0000U+10FFFF

U+<hex>是代码点的格局,其中 U+ 是示意 Unicode 的前缀,<hex>是十六进制的数字。例如,U+0041U+2603

请记住,代码点就是一个简略的数字。你应该这样想,代码点是元素在数组中的一个索引。

因为 Unicode 将一个代码点与一个字符相关联,所以产生了神奇的成果。例如,U+0041对应于名为拉丁大写字母(LATIN CAPITAL LETTERA的字符(渲染为 A),或者U+2603 对应于名为雪人(SNOWMAN)的字符(渲染为).

并非所有代码点都具备相应的字符。1114112 个代码点可用(范畴从 U +0000 到 U +10FFFF),但只有 137929 个代码点调配了字符(截至 2019 年 5 月)。

2.2 Unicode 立体

“立体(Plane)是一个从 U+n0000U+nFFFF,总共有 65536 个继续的 Unicode 代码点的范畴,其中 n 取值范畴是0x0~0x10

立体将 Unicode 代码点分成 17 个相等的组:

  • 立体 0 蕴含从 U+0000U+FFFF的代码点,
  • 立体 1 蕴含从 U+10000U+1FFFF的代码点
  • 立体 16 蕴含从 U+100000U+10FFFF的代码点

根本多文种立体

立体 0 是一个非凡的立体,称为根本多文种立体(Basic Multilingual Plane)或简称BMP。它蕴含来自大多数古代语言(根本拉丁语)、西里尔语)、希腊语等)的字符和大量符号。

如上所述,根本多文种立体的代码点在 U+0000U+FFFF之间,最多能够有 4 个十六进制数字。

开发人员通常解决 BMP 中的字符。它蕴含大多数必须的字符。

BMP 中的某些字符:

  • eU+0065,命名为 拉丁文小写字母 e
  • |U+007C,命名为 竖线
  • U+25A0,命名为 彩色正方形
  • U+2602,命名为伞

星形立体

其余 16 个超过 BMP 的立体(立体 1、立体 2、… 立体 16)被称为 星形立体 (astral planes)或者 辅助立体(supplementary planes)。

星形立体里的代码点被称为星形代码点,它的范畴从 U+10000U+10FFFF

星形代码点能够有 5 到 6 个十六进制数字,如 U+dddddU+dddddd

例子如下:

  • 𝄞U+1D11E,命名为 音乐符号 G 谱号
  • 𝐁U+1D401,命名为 数学黑体大写字母 B
  • 🀵U+1F035,命名为 多米诺程度题目 -00-04
  • 😀U+1F600,命名为 笑脸

2.3 代码单元

计算机在内存中不应用代码点或形象字符。它须要一种物理形式来示意 Unicode 代码点:代码单元(code units)。

“代码单元是一个位序列,用于对给定编码模式中的每个字符进行编码。”

字符编码将形象代码点转换为物理位:代码单元。

换句话说,字符编码将 Unicode 代码点转换为惟一的代码单元序列。

风行的编码有 UTF-8、UTF-16 和 UTF-32。

大多数 JavaScript 引擎应用 UTF-16 编码。这会影响 JavaScript 应用 Unicode 的形式。从当初开始,让咱们专一于UTF-16

UTF-16(长名称:16 位 Unicode 转换格局)是一种可变长度编码:

  • BMP 中的代码点应用 16 位的单个代码单元进行编码
  • 星形代码点应用两个 16 位的编码单元进行编码。

咱们来举几个例子。

假如你想将拉丁小写字母 a 保留到硬盘驱动器。Unicode 通知你 丁小写字母 a 映射到 U+0061 代码点。

当初让咱们询问 UTF-16 编码 U +0061 应该如何转换。编码标准规定,对于 BMP 代码点,取其十六进制数 U +0061,并将其存储到一个 16 位的代码单元中:0x0061

如你所见,BMP中的代码点适宜于单个 16 位代码单元。

2.4 代理对

当初让咱们钻研一个简单的案例。假如你要保留一个星形代码点(来自星形立体):笑脸😀。此字符映射到 U+1F600 代码点。

因为星形代码点须要 21 位来保存信息,因而 UTF-16 示意你须要两个 16 位的代码单元。代码点 U+1F600 被分成所谓的代理对:0xD83D(高代理代码单元,high-surrogate code unit)和 0xDE00(低代理代码单元,low-surrogate code unit)。

援用
代理对 (Surrogate pair) 是单个形象字符的示意,它由两个 16 位代码单元的代码单元序列组成,其中该对的第一个值是高代理代码单元,第二个值是低代理代码单元。”

星形代码点须要两个代码单元:代理对 。正如您在后面的示例中看到的那样,要在 UTF-16 中对 U+1F600 (😀) 进行编码,将应用代理对:0xD83D 0xDE00

console.log('\uD83D\uDE00'); // => '😀'

高代理项代码单元取值范畴为 0xD8000xDBFF。低代理代码单元取值范畴为 0xDC000xDFFF

将代理对转换为星形代码点的算法如下所示,反之亦然:

function getSurrogatePair(astralCodePoint) {
  let highSurrogate = 
     Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xD800;
  let lowSurrogate = (astralCodePoint - 0x10000) % 0x400 + 0xDC00;
  return [highSurrogate, lowSurrogate];
}
getSurrogatePair(0x1F600); // => [0xD83D, 0xDE00]
function getAstralCodePoint(highSurrogate, lowSurrogate) {return (highSurrogate - 0xD800) * 0x400 
      + lowSurrogate - 0xDC00 + 0x10000;
}
getAstralCodePoint(0xD83D, 0xDE00); // => 0x1F600

解决代理对并不难受。在 JavaScript 中解决字符串时,您必须将它们作为非凡状况解决,如下文所述。

然而,UTF-16 在内存中是高效的。99% 的字符来自BMP,这些字符只须要一个代码单元。

组合标记

在一个特定的书写零碎的上下文中,一个字形(grapheme)或符号(symbol)是一个最小的独特的书写单位。

字形是从用户的角度对待字符。屏幕上显示的一个图形的具体图像称为 字形(glyph)

在大多数状况下,单个 Unicode 字符示意单个图形。例如,U+0066 拉丁文小写字母示意英文字母 f

在某些状况下,字形蕴含一系列字符。

例如,å 是丹麦书写零碎中的一个原子字形。它应用 U+0061 拉丁文小写字母 A(出现为 A)和特殊字符U+030A(出现为 ◌̊)COMBINING RING ABOVE).

U+030A 润饰前置字符,命名为 组合标记(combining mark)。

console.log('\u0061\u030A'); // => 'å'
console.log('\u0061');       // => 'a'

组合标记 是一个利用于前一个根本字符的字符,用于创立字形。”

组合标记包含重音符号、变音符号、希伯来文点、阿拉伯元音符号和印度语字母等字符。

组合标记通常在没有根本字符的状况下不独自应用。您应该防止独自显示它们。

与代理对一样,组合标记在 JavaScript 中也很难解决。

组合字符序列(根本字符 + 组合标记)被用户辨别为单个符号(例如 '\u0061\u030A''å')。然而开发者必须确定应用U+0061U+030A 这 2 个代码点来结构 å

3. JavaScript 中的 Unicode

ES2015 标准 提到 源代码文本应用 Unicode(5.1 及更高版本)示意。源文本是从 U+0000 U+10FFFF 的代码点序列。源代码的存储或替换形式与 ECMAScript 标准无关,但通常以 UTF-8(web 首选编码方式)编码。

我倡议应用 Basic Latin Unicode block)(或 ASCII)中的字符保留源代码文本。ASCII 以外的字符应本义。这将确保编码方面的问题更少。

在外部,在语言层面,ECMAScript 2015 提供了一个明确的定义,JavaScript 中的字符串是什么:

字符串类型是零或多个 16 位无符号整数值(“元素”)的所有有序序列的汇合,最大长度为 (2 的 53 次方减 1) 个元素。字符串类型通常用于示意正在运行的 ECMAScript 程序中的文本数据,在这种状况下,字符串中的每个元素都被视为 UTF-16 代码单位值。

字符串的每个元素都被引擎解释为一个代码单元。字符串的出现形式不能确定它蕴含哪些代码单元(代表代码点)。请参阅以下示例:

console.log('cafe\u0301'); // => 'café'
console.log('café');       // => 'café'

'cafe\u0301''café' 文字的代码单元稍有不同,但它们都出现为雷同的符号序列café

字符串的长度 是其中的元素 (16 位的代码单元) 的数目。[…]当 ECMAScript 操作解释字符串值时,每个元素被解释为单个 UTF-16 代码单元。

正如你从上述章节的 代理对 组合标记 中晓得的那样,某些符号须要 2 个或更多代码单元来示意。所以在统计字符数或按索引拜访字符时要留神:

const smile = '\uD83D\uDE00';
console.log(smile);        // => '😀'
console.log(smile.length); // => 2
const letter = 'e\u0301';
console.log(letter);        // => 'é'
console.log(letter.length); // => 2

smile 字符串蕴含 2 个代码单元:\uD83D(高代理)和 \uDE00(低代理)。因为字符串是一系列代码单元,smile.length 的计算结果为 2。即便渲染的 smile 仅有一个符号 '😀'

letter字符串也是雷同的状况。组合标记 U+0301 利用在前一个字符 e 上,渲染后果为符号'é'。然而 letter 蕴含 2 个代码单元,因而 letter.length 为 2。

我的倡议:始终将 JavaScript 中的字符串视为一系列代码单元。字符串的出现形式无奈分明阐明它蕴含哪些代码单元。

星形立体的符号和组合字符序列须要编码 2 个或更多的代码单元。但它们被视为一个繁多的字形(grapheme)。

如果字符串具备 代理对 组合标记,开发人员在没有记住这一要点的状况上来计算字符串的长度或者按索引拜访字符时会感觉到困惑。

大多数 JavaScript 字符串办法都不反对 Unicode。如果字符串蕴含复合 Unicode 字符,请在调用 myString.slice()myString.substring() 等时采取预防措施。

3.1 转义序列

JavaScript 字符串中的转义序列用于示意基于代码点编号的代码单元。JavaScript 有 3 种本义类型,一种是在 ECMAScript 2015 中引入的。

让咱们更具体地理解它们。

十六进制转义序列

最短的模式被命名为十六进制转义序列:\x<hex>,其中 \x 是一个前缀,后跟一个固定长度为 2 位的十六进制数字 <hex>
例如'\x30'(符号 ’0’)或'\x5B'(符号 '[‘)。

字符串文字或正则表达式中的十六进制转义序列如下所示:

const str = '\x4A\x61vaScript';
console.log(str);                    // => 'JavaScript'
const reg = /\x4A\x61va.*/;
console.log(reg.test('JavaScript')); // => true

十六进制转义序列能够本义无限范畴内的代码点:从 U+00 U+FF,因为只容许应用 2 位数字。然而十六进制本义很好,因为它很短。

Unicode 转义序列

如果你想本义整个 BMP 中的代码点,请应用 unicode 转义序列 。本义格局为 \u<hex>,其中 \u 是前缀后跟一个固定长度为 4 位的十六进制数 <hex>。例如'\u0051'(符号 ’Q’)或'\u222B'(积分符号 ’∫’)。

让咱们应用 unicode 转义序列:

const str = 'I\u0020learn \u0055nicode';
console.log(str);                 // => 'I learn Unicode'
const reg = /\u0055ni.*/;
console.log(reg.test('Unicode')); // => true

Unicode 转义序列能够本义无限范畴内的代码点:从 U+0000U+FFFF(所有 BMP 代码点),因为只容许应用 4 位数字。大多数状况下,这足以示意罕用的符号。

要在 JavaScript 文字中批示星形立体的符号,请应用两个连贯的 unicode 转义序列(高代理和低代理),这将创立代理对:

const str = 'My face \uD83D\uDE00';
console.log(str); // => 'My face 😀'

代码点转义序列

ECMAScript 2015 提供了示意整个 Unicode 空间的代码点的转义序列:U+0000 U+10FFFF,即 BMP 星形立体

新格局称为代码点转义序列:\u{<hex>},其中 <hex> 是一个长度为 1 到 6 位的十六进制数。

例如'\u{7A}'(符号 ’z’)或'\u{1F639}'(笑脸猫符😹)。

const str = 'Funny cat \u{1F639}';
console.log(str);                      // => 'Funny cat 😹'
const reg = /\u{1F639}/u;
console.log(reg.test('Funny cat 😹')); // => true

请留神,正则表达式 /\u{1F639}/u 有一个非凡标记u,它启用额定的 Unicode 性能。(无关详细信息,请参见 3.5 正则表达式匹配。)

我喜爱代码点转义序列来示意星形符号,而不是代理对。

让咱们来本义带光环的笑脸符号😇U+1F607代码点。

const niceEmoticon = '\u{1F607}';
console.log(niceEmoticon);   // => '😇'
const spNiceEmoticon = '\uD83D\uDE07'
console.log(spNiceEmoticon); // => '😇'
console.log(niceEmoticon === spNiceEmoticon); // => true

调配给变量 niceEmoticon 的字符串文字有一个代码点本义符 '\u{1F607}' ,示意一个星体代码点 U+1F607。接着,创立了一个代理对(2 个代码单元)。如您所见,spNiceEmoticon 是应用一对 unicode 本义符 '\uD83D\uDE07' 的代理对
创立的,它等于 niceEmoticon

当应用 RegExp 构造函数创立正则表达式时,在字符串文字中,您必须将每个 \ 替换为 \\ ,示意这是 unicode 本义。以下正则表达式对象是等效的:

const reg1 = /\x4A \u0020 \u{1F639}/;
const reg2 = new RegExp('\\x4A \\u0020 \\u{1F639}');
console.log(reg1.source === reg2.source); // => true

字符串比拟

JavaScript 中的字符串是代码单元序列。能够正当地预期,字符串比拟波及对匹配的代码单元进行求值。

这种办法疾速无效。它能够很好地解决“简略”字符串:

const firstStr = 'hello';
const secondStr = '\u0068ell\u006F';
console.log(firstStr === secondStr); // => true

firstStrsecondStr 字符串具备雷同的代码单元序列。它们是相等的。

假如您要比拟出现的两个字符串,它们看起来雷同但蕴含不同的代码单元序列。那么你可能会失去一个意想不到的后果,因为在比拟中看起来雷同的字符串并不相等:

渲染时 str1 str2 看起来雷同,但具备不同的代码单元。
产生这种状况是因为 ç 字形能够通过两种形式构建:

  • 应用U+00E7,带有变音符的拉丁小写字母 c
  • 或者应用组合字符序列:U+0063拉丁小写字母 c,加上组合标记 U+0327 组合变音符。

如何解决这种状况并正确比拟字符串?答案是字符串规范化。

规范化

规范化(Normalization)是将字符串转换为标准示意,以确保标准等效(和 / 或兼容性等效)字符串具备惟一示意。

换句话说,当字符串具备组合字符序列或其余复合构造的简单构造时,您能够将其规范化为标准模式。规范化的字符串能够轻松比拟或执行文本搜寻等字符串操作。

Unicode 规范附录 #15 提供了无关规范化过程的乏味细节。

在 JavaScript 中,要规范化字符串,请调用 myString.normalize([normForm]) 办法,该办法在 ES2015 中提供。normForm是一个可选参数(默认为“NFC”),能够采纳以下规范化模式之一:

  • 'NFC' 作为规范化模式的规范组合
  • 'NFD' 作为规范化模式标准合成
  • 'NFKC'作为规范化模式兼容性组合
  • 'NFKD'作为规范化模式兼容性合成

让咱们通过利用字符串规范化来改良后面的示例,这将容许正确比拟字符串:

const str1 = 'ça va bien';
const str2 = 'c\u0327a va bien';
console.log(str1 === str2.normalize()); // => true
console.log(str1 === str2);             // => false

'ç''c\u0327' 在标准上是等价的。
当调用 str2.normalize() 时,将返回 str2 的标准版本('c\u0327' 被替换为 'ç')。所以比拟 str1 === str2.normalize() 按预期返回 true

str1 不受规范化的影响,因为它曾经是标准模式了。

规范化两个比拟的字符串,以取得两个操作数上的标准示意仿佛是正当的。

3.3 字符串长度

确定字符串长度的罕用办法当然是访 myString.length 属性。此属性示意字符串具备的代码单元数。

属于 BMP 的代码点的字符串长度的计算通常按预期失去:

const color = 'Green';
console.log(color.length); // => 5

color字符串中的每个代码单元对应着一个独自的字素。字符串的预期长度为5

长度和代理对

当字符串蕴含代理对来示意星形代码点时,状况变得辣手。因为每个代理对蕴含 2 个代码单元(高代理和低代理),因而长度属性大于预期。

看一个例子:

const str = 'cat\u{1F639}';
console.log(str);        // => 'cat😹'
console.log(str.length); // => 5

str 字符串被渲染时,它蕴含 4 个符号 cat😹。然而,str.length 的计算结果为 5,因为 U+1F639 是用 2个代码单元(代理对)编码的星形代码点。

可怜的是,目前还没有解决该问题的原生的和高性能的办法。

至多 ECMAScript 2015 引入了辨认星形符号的算法。星形符号被视为单个字符,即便应用 2 个代码单元进行编码。

字符串迭代器 String.prototype[@@iterator]()是反对 Unicode 的。您能够将字符串与扩大运算符 [...str] Array.from(str) 函数联合应用(两者都应用字符串迭代器)。而后计算返回数组中的符号数。

请留神,此解决方案在宽泛应用时可能会造成轻微的性能问题。

让咱们用扩大运算符改良下面的例子:

const str = 'cat\u{1F639}';
console.log(str);             // => 'cat😹'
console.log([...str]);        // => ['c', 'a', 't', '😹']
console.log([...str].length); // => 4

长度和组合标记

那么组合字符序列呢?因为每个组合标记都是一个代码单元,所以您可能会遇到雷同的艰难。

该问题在规范化字符串时失去解决。如果侥幸的话,组合字符序列将规范化为单个字符。让咱们试试:

const drink = 'cafe\u0301';
console.log(drink);                    // => 'café'
console.log(drink.length);             // => 5
console.log(drink.normalize())         // => 'café'
console.log(drink.normalize().length); // => 4

Drink 字符串蕴含 5 个代码单元(因而 drink.length 为 5),即便渲染它也显示 4 个符号。

可怜的是,规范化不是一个通用的解决方案。长组合字符序列在一个符号中并不总是具备标准的等价物。让咱们看看这样的案例:

const drink = 'cafe\u0327\u0301';
console.log(drink);                    // => 'cafȩ́'
console.log(drink.length);             // => 6
console.log(drink.normalize());        // => 'cafȩ́'
console.log(drink.normalize().length); // => 5

Drink 6 个代码单元,drink.length 的计算结果为 6。然而,drink4个符号。

规范化 Drink.normalize() 将组合序列 'e\u0327\u0301' 转换为两个字符 'ȩ\u0301' 的标准模式(通过仅删除一个组合标记)。遗憾的是,drink.normalize().length 的计算结果为 5,但依然没有示意正确的符号数。

字符定位

因为字符串是一系列代码单元,因而通过索引拜访字符串中的字符也存在艰难。

当字符串仅蕴含 BMP 字符时(不包含从 U+D800U+DBFF 的高代理和从 U+DC00U+DFFF 的低代理),字符定位没有什么问题。

const str = 'hello';
console.log(str[0]); // => 'h'
console.log(str[4]); // => 'o'

每个符号都应用单个代码单元进行编码,因而通过索引拜访字符串字符是正确的。

字符定位和代理对

当字符串蕴含星形符号时,状况会发生变化。

星形符号应用 2 个代码单元(代理对)进行编码。因而通过索引拜访字符串字符可能会返回一个分隔的高代理或低代理,它们是有效符号。

以下示例拜访星形符号中的字符:

const omega = '\u{1D6C0} is omega';
console.log(omega);        // => '𝛀 is omega'
console.log(omega[0]);     // => '' (unprintable symbol)
console.log(omega[1]);     // => '' (unprintable symbol)

因为 U+1D6C0 大写字母 OMEGA(MATHEMATICAL BOLD CAPITAL OMEGA)是一个星形字符,所以它应用 2 个代码单元的代理对进行编码。omega[0]拜访高代理项代码单元,omega[1]拜访低代理项,从而拆散代理对。

在一个字符串中存在 2 种正确拜访星形符号的可能性:

  • 应用字符串迭代器并生成符号数组[…str][index]
  • 应用 number = myString.codePointAt(index) 获取代码点编号,而后应用 String.fromCodePoint(number)(举荐选项)将数字转换为符号。

让咱们同时利用这两个选项:

const omega = '\u{1D6C0} is omega';
console.log(omega);                        // => '𝛀 is omega'
// Option 1
console.log([...omega][0]);                // => '𝛀'
// Option 2
const number = omega.codePointAt(0);
console.log(number.toString(16));          // => '1d6c0'
console.log(String.fromCodePoint(number)); // => '𝛀'

[…omega]返回 omega 字符串蕴含的符号数组。代理对的计算是正确的,因而拜访第一个字符的成果与预期的一样。[...smile][0] '𝛀'

omega.codePointAt(0) 办法调用是反对 Unicode 的,因而它返回 omega 字符串中第一个字符的星形代码点编号0x1D6C0。函数 String.fromCodePoint(number) 返回基于代码点编号的符号:'𝛀'

字符定位和组合标记

带有组合标记的字符串中的字符定位与上述字符串长度存在雷同的问题。

通过字符串中的索引拜访字符就是拜访代码单元。然而,组合标记序列应该作为一个整体来拜访,而不是分成独自的代码单元。

上面的例子演示了这个问题:

const drink = 'cafe\u0301';  
console.log(drink);        // => 'café'
console.log(drink.length); // => 5
console.log(drink[3]);     // => 'e'
console.log(drink[4]);     // => ◌́

Drink[3] 只拜访根本字符 e,没有组合标记 U+0301 COMBINING ACUTE ACCENT(出现为 ◌́ )。

Drink[4] 拜访孤立的组合标记 ◌́

在这种状况下,利用字符串规范化。组合字符序列 U+0065 LATIN SMALL LETTER e U+0301 COMBINING ACUTE ACCENT ◌́ 具备规范等价物 U+00E9 LATIN SMALL LETTER E WITH ACUTE é。让咱们改良后面的代码示例:

const drink = 'cafe\u0301';
console.log(drink.normalize());        // => 'café'  
console.log(drink.normalize().length); // => 4  
console.log(drink.normalize()[3]);     // => 'é'

请留神,并非所有组合字符序列都具备作为单个符号的规范等价物。所以规范化字符串计划并不通用。
侥幸的是,它应该实用于欧洲 / 北美语言的大多数状况。

正则表达式匹配

正则表达式和字符串一样,都是依照代码单元运行的。与之前形容的场景相似,这在解决代理对和应用正则表达式组合字符序列时会产生艰难。

BMP 字符按预期匹配,因为单个代码单元示意一个符号:

const greetings = 'Hi!';
const regex = /.{3}/;
console.log(regex.test(greetings)); // => true

greetings有 3 个字符,即 3 个代码单元。正则表达式 /.{3}/ 表达式能胜利匹配。

在匹配星形符号(用 2 个代码单元的代理对编码)时,您可能会遇到困难:

const smile = '😀';
const regex = /^.$/;
console.log(regex.test(smile)); // => false

smile字符串蕴含星形符号 U+1F600 GRINNING FACEU+1F600 应用代理对 0xD83D 0xDE00 进行编码。
然而,正则表达式 /^.$/ 冀望匹配一个代码单元,所以失败了。
用星形符号定义字符类时状况更糟。JavaScript 抛出一个谬误:

const regex = /[😀-😎]/;
// => SyntaxError: Invalid regular expression: /[😀-😎]/: 
// Range out of order in character class

星形代码点被编码为代理对。因而 JavaScript 应用代码单元 /[\uD83D\uDE00-\uD83D\uDE0E]/ 来示意正则表达式。每个代码单元都被视为模式中的一个独自元素,因而正则表达式疏忽了代理对的概念。

字符类的 \uDE00-\uD83D 局部有效,因为 \uDE00 大于 \uD83D。后果,正则表达式产生谬误。

正则表达式 u 标记

侥幸的是,ECMAScript 2015 引入了一个有用的 u 标记,使正则表达式可能辨认 Unicode。该标记能够正确处理星形符号。

您能够在正则表达式 /u{1F600}/u 中应用 unicode 转义序列。此本义比批示高代理和低代理对 /\uD83D\uDE00/ 更短。

让咱们利用 u 标记,看看 . 运算符(包含量词 ?, +, * {3}, {3,}, {2,3})如何匹配星形符号:

const smile = '😀';
const regex = /^.$/u;
console.log(regex.test(smile)); // => true

/^.$/u 正则表达式,因为 u 标记,反对匹配 Unicode,当初就能匹配星形字符 😀

u 标记也能够正确处理字符类中的星形符号:

const smile = '😀';
const regex = /[😀-😎]/u;
const regexEscape = /[\u{1F600}-\u{1F60E}]/u;
const regexSpEscape = /[\uD83D\uDE00-\uD83D\uDE0E]/u;
console.log(regex.test(smile));         // => true
console.log(regexEscape.test(smile));   // => true
console.log(regexSpEscape.test(smile)); // => true

[😀-😎]匹配一个范畴内的星形字符,能够匹配'😀'

正则表达式和组合标记

可怜的是,无论有没有 u 标记,正则表达式都会将组合标记视为独自的代码单元。

如果须要匹配组合字符序列,则必须别离匹配基字符和组合标记。

看看上面的例子:

const drink = 'cafe\u0301';
const regex1 = /^.{4}$/;
const regex2 = /^.{5}$/;
console.log(drink);              // => 'café'  
console.log(regex1.test(drink)); // => false
console.log(regex2.test(drink)); // => true

字符串被渲染成 4 个字符 café
然而,正则表达式匹配 'cafe\u0301' 作为 5 个元素的序列 /^.{5}$/.

4. 总结

在 JavaScript 中,对于 Unicode 最重要的概念可能是将字符串视为代码单元序列,因为它们实际上是这样的。

当开发者认为字符串是由字素(或符号)组成,而疏忽了代码单元序列概念时,就会呈现混同。

它在解决蕴含代理对或组合字符序列的字符串时会产生误解:

  • 获取字符串长度
  • 字符定位
  • 正则表达式匹配

请留神,JavaScript 中的大多数字符串办法并不齐全反对 Unicode:如 myString.indexOf()myString.slice() 等。

ECMAScript 2015 引入了一些不错的性能,例如字符串和正则表达式中的代码点转义序列 \u{1F600}

新的正则表达式标记 u 反对辨认 Unicode 的字符串匹配。它使匹配星形符号变得更简略。

字符串迭代器 String.prototype[@@iterator]() 是 反对 Unicode。您能够应用扩大运算符 [...str]Array.from(str) 创立符号数组,并在不毁坏代理对的状况下计算字符串长度或按索引拜访字符。请留神,这些操作会对性能产生一些影响。

如果您须要更好的办法来解决 Unicode 字符,您能够应用 punycode 库或 generate 库生成专门的正则表达式。

心愿这篇文章对你把握 Unicode 有帮忙!

退出移动版