length统计utf-16编码单元数而非字符数,导致代理对字符(如?、?)length为2;应使用array.from()、intl.segmenter或codepointat()获取真实字符数。

原始字符串的 length 属性并不统计“人眼看到的字符个数”,而是统计 UTF-16 编码单元(code unit)的数量。这个设计在遇到 Unicode 代理对时,就会产生直观上的“冲突”——一个视觉上完整的字符,length 却返回 2。
length 统计的是编码单元,不是字符
JavaScript 和 .NET(如 C# 的 string.Length)都将字符串内部表示为 UTF-16 序列,每个 char 或“代码单元”占 16 位。绝大多数常用字符(如英文字母、常见汉字)只需一个单元,length 值与字符数一致:
-
"a".length→ 1 -
"你好".length→ 2
但 Unicode 中编号超过 0xFFFF 的字符(如大部分 emoji、古汉字、奥塞治文等),无法用单个 16 位值表示,必须拆成两个连续的单元:一个高位代理(high surrogate,范围 0xD800–0xDBFF),一个低位代理(low surrogate,范围 0xDC00–0xDFFF)。这两个单元合起来才代表一个 Unicode 码点(code point)。
此时 length 会把这两个单元都计入,导致数值翻倍:
-
"?".length→ 2(U+1F44D,需代理对) -
"?".length→ 2(U+2070E,增补平面汉字) -
"?".length→ 2(奥塞治字母,同理)
冲突带来的实际问题
这种“统计口径不一致”会在业务逻辑中引发明显异常:
- 用户输入一个 ?,却被判定为“2 字符”,违反“昵称最多 10 字”限制
- 用
str.substring(0, 5)截取前 5 个位置,可能恰好切在代理对中间,返回乱码(如"") - 遍历字符串用
str[i]取字符时,代理对的高位和低位被当作两个独立“字符”处理 -
charAt()和charCodeAt()同样按 code unit 索引,无法直接获取完整码点
如何绕过这个冲突
要获得符合人类认知的“字符数”,应跳过底层 UTF-16 单元,直接按 Unicode 码点或图形单元(grapheme cluster)计数:
- 基础准确:用扩展运算符或
Array.from()—— 它们基于字符串的迭代器协议,自动识别代理对Array.from("?").length→ 1[..."?"].length→ 1 - 需要支持组合字符(如
é写作e + ◌́)或 ZWJ 连接序列(如家庭 emoji????):使用Intl.Segmenternew Intl.Segmenter('zh', {granularity: 'grapheme'}).segment("??").length→ 1 - 逐码点操作:用
codePointAt()替代charCodeAt(),配合String.fromCodePoint()构造字符
C# 中的对应表现
.NET 的 string.Length 行为完全一致:它返回 char 实例数量,而非 Unicode 字符数。例如:
-
"Hello".Length→ 5 -
"你好".Length→ 2 -
"??".Length→ 4(每个奥塞治字母占 2 个char)
若需真实 Unicode 字符数,C# 推荐使用 System.Globalization.StringInfo 或 System.Text.Rune 类型(.NET Core 3.0+)来安全遍历码点。











