什么是代理对 (Surrogate Pair)
本文约 4 分钟读完。
输入一个 emoji 却被计为「2 个字符」。这背后的原因就是代理对。当用 UTF-16 这种较早的编码设计来处理 Unicode 的表示范围时,有些字符不得不用两个「半片」来表示。 这对「半片」的组合就叫做代理对。它是处理 emoji 和部分汉字时必然会遇到的基础概念。
定义
代理对是 UTF-16 中用两个 16 位单元来表示超过 U+FFFF 的 Unicode 码位的机制。 前半部分称为「高位代理」(High Surrogate, U+D800-U+DBFF),后半部分称为「低位代理」(Low Surrogate, U+DC00-U+DFFF), 两者必须成对出现。
为什么需要它
Unicode 拥有从 U+0000 到 U+10FFFF 的广阔码位空间,表示这些需要 21 位。 然而 UTF-16 每个单元只有 16 位,单独只能表示到 U+FFFF。 为了处理超出这个范围的字符 (大部分 emoji、部分汉字、古文字、数学符号等),引入了高位和低位组合的扩展机制。
与 emoji 的关系
emoji 的码位大多位于 U+1F000 以后的区域,在 UTF-16 中以代理对形式表示。 例如 🌸 (U+1F338) 在 UTF-16 中存储为 D83C DF38 两个 16 位单元。 这就是 JavaScript 中一个 emoji 的字符串长度为 2 的直接原因。
JavaScript 的行为
JavaScript 的 String 内部以 UTF-16 表示,因此 .length 会将代理对计为 2。
| 表达式 | 结果 | 含义 |
|---|---|---|
"あ".length | 1 | 在 BMP 内,无需代理对 |
"🌸".length | 2 | 由代理对构成 |
[..."🌸"].length | 1 | 按码位单位分解 |
"🌸".codePointAt(0) | 127800 | 组合后的码位值 |
与 UTF-8 / UTF-32 的区别
代理对是 UTF-16 特有的机制。UTF-8 用 1-4 字节的可变长度表示所有码位,UTF-32 用固定 4 字节处理所有码位。 在使用 UTF-8 或 UTF-32 的语言和处理系统中,不会出现代理对的概念。
实务中的应用场景
- 字符数验证:当表单需要以「用户感知的字符单位」计数最大字符数时,需要理解代理对
- 文本处理:子字符串截取、反转处理、正则表达式匹配中,需要将代理对作为一个单位处理
- 文件名和数据库:旧系统无法正确处理代理对,导致包含 emoji 的数据损坏
- 社交媒体字符计数:各平台计数逻辑的差异源于对代理对和字素簇的不同处理方式
正确计数的方法
如果想按用户认知的「1 个字符」为单位计数,需要以字素簇而非代理对为单位。 现代 JavaScript 中可以使用标准的 Intl.Segmenter,它能正确地将 emoji 和 ZWJ 序列视为 1 个单位。
常见误解
- ❌「emoji 全部被算作 2 个字符」→ ✅ U+FFFF 以下的 emoji (部分旧符号类) 无需代理对,算 1 个字符
- ❌「代理对在 UTF-8 中也会用到」→ ✅ 这是 UTF-16 特有的机制
- ❌「高位代理单独也是有效字符」→ ✅ 单独出现时是无效的 Unicode 字符串 (Unpaired Surrogate)