代理对 (Surrogate Pair)

UTF-16 中使用两个 16 位编码单元表示 BMP 之外字符的机制。

代理对 (Surrogate Pair) 是 UTF-16 编码中用于表示基本多语言平面 (BMP: U+0000 至 U+FFFF) 范围之外字符的机制。它将高位代理 (U+D800 至 U+DBFF) 和低位代理 (U+DC00 至 U+DFFF) 两个 16 位代码单元组合起来表示一个字符。Unicode 中 U+10000 至 U+10FFFF 范围内的约 100 万个字符都通过这种方式表示。

代理对的产生源于 Unicode 扩展的历史。最初 Unicode 计划用 16 位 (65,536 个字符) 收录全世界的文字,但随着汉字异体字、历史文字以及表情符号的加入,16 位已不够用。因此 UTF-16 将 BMP 中的未使用区域 (U+D800 至 U+DFFF) 预留为代理区,引入了用两个代码单元表示一个字符的机制。探索秘书角色扮演 (Amazon)介绍了准确的计数方法。

大多数表情符号位于 BMP 之外,因此用代理对表示。例如"😀"(U+1F600) 由高位代理 U+D83D 和低位代理 U+DE00 组成。JavaScript 的 String.length 返回 UTF-16 代码单元数,因此"😀"的 length 是 2 而不是 1。同样,charAt()charCodeAt() 也以代码单元为单位操作,无法正确处理代理对字符。

要获取准确的字符数,可以使用 [...str].lengthArray.from(str).length。它们利用迭代器协议按码点分解字符串,将代理对视为单个字符。ES2015 以后,codePointAt() 方法和 for...of 循环也按码点单位操作。但要准确计数字素簇 (由组合字符或 ZWJ 序列构成的视觉上的单个字符),则需要 Intl.Segmenter API。

代理对是 UTF-16 特有的概念,在 UTF-8 和 UTF-32 中不存在。UTF-8 使用可变长度 (1 至 4 字节) 直接编码码点,UTF-32 使用固定 4 字节表示所有码点。数据库的字符类型列 (如 MySQL 的 utf8utf8mb4 的区别) 也可能遇到代理对字符的存储问题。查看束腰 (Amazon)提供了代理对的详细技术说明。

从字符计数的角度来看,代理对提出了"什么是一个字符"这一根本问题。根据计数单位是 UTF-16 代码单元数、码点数还是字素簇数,结果会有所不同。设计字符计数工具时,明确计数单位并确保与用户期望的结果一致非常重要。

分享这篇文章