ES 中的 Unicode

published

ES 采用的字符集

ES 采用 Unicode 字符集。

ES 使用的 Unicode 编码方案

  • 1995 年 5 月,JavaScript 设计完成。当时只有 UCS-2 一种编码方案可用。
  • 1996 年 7 月,UTF-16 编码方案发布。
  • 1996 年 10 月,第一个 JavaScript 解释器问世。由于语言设计已经完成,解释器依从设计来实现,所以同样采用了 UCS-2 编码方案。
  • 1996 年 11 月,Netscape 向 ECMA 提交语言标准。同样的,仍旧采用 UCS-2 编码方案。

这样来讲,ES 使用的是 UCS-2 编码方案。

但是,UTF-16 编码方案发布时,明确宣布是 UCS-2 编码方案的超集(即基本多语言平面 Code Point 沿用 UCS-2 编码方案的 2 字节编码,辅助平面 Code Point 使用 4 字节编码)。UCS-2 被整合进 UTF-16 后,UTF-16 成为事实标准。

综上,ES 使用的是 UTF-16 编码方案。

从 ES 3 开始,标准文档中就已经开始涉及 UTF-16 相关的描述。

ES 中字符处理的局限

ES 使用的是 UTF-16 编码方案,按理来说,是可以处理 4 字节编码的。但是由于上一节提到的历史原因,ES 2015 之前的标准中所定义的字符处理函数只能处理 2 字节编码,4 字节编码会被当成两个 2 字节编码来处理。

以辅助平面字符 𠀾 为例:

  • Unicode 码点为 U+2003E
  • UTF-16 编码为 0xD840DC3E

在处理这个字符时会遭遇诸多问题:

const c = '𠀾'

c.length        // 2  错误:字符长度应该为 1
c.charAt(0)     // �  错误:第一个字符应该为 𠀾
c.charCodeAt(0) // 55360(0xD840) 错误:第一个字符对应的编码应该为 0xD840DC3E

类似的问题存在于 ES2015 之前的所有字符处理函数中:

  • String.prototype.replace()
  • String.prototype.substring()
  • String.prototype.slice()

要解决字符处理的这些问题,只能依照 UTF-16 编码方案自行处理。

ES 2015 增强对 UTF-16 的支持

正确识别 4 字节编码

迭代器支持:

for (const s of string) {
}

为了保持兼容性,length 属性仍然保持原来的行为。为了得到字符串的正确长度,可以使用下面的方法:

Array.from(string).length

Unicode Code Point 转义

'\u{2003E}'

添加字符串处理函数

  • String.fromCodePoint():输入 Unicode Code Point,返回对应字符。
  • String.prototype.codePointAt():输入字符,返回对应码点。
  • String.prototype.at():返回字符串特定位置的字符。

增强正则表达式

为正则表达式添加 u 修饰符,以正式支持 UTF-16 编码。

/^.$/.test('𠀾')  // false
/^.$/u.test('𠀾') // true

Unicode 正规化

有些字符除了字母以外,还有附加符号。比如,汉语拼音的 Ǒ,字母上面的声调就是附加符号。对于许多欧洲语言来说,声调符号是非常重要的。 Uncode 对需要附加符号的情况提供了两种表示方法:

  1. 带附加符号的单个字符,即使用一个 Code Point 表示一个字符。
  2. 将附加符号与主体字符复合显示的复合字符,即使用两个 Code Point 表示一个字符。

以 Ǒ 为例:

// 方法 1
'\u01D1' // Ǒ

// 方法 2
'\u004F\u030C' // Ǒ

以上两种表示方法,在视觉和语义上都完全相同,理应等价处理。但,ES 无法辨别。

'\u01D1'==='\u004F\u030C' // false

为了解决问题,引入了 String.prototype.normalize() 方法,以允许 Unicode 正规化(为不同的表示方法生成唯一的 Code Point)。

'\u01D1'.normalize() === '\u004F\u030C'.normalize() // true

参考