JavaScript 字符串的 length 属性统计 UTF-16 编码单元数而非 Unicode 码点数,导致 emoji、代理对等字符长度失真;应使用扩展运算符或 Intl.Segmenter 获取语义化长度。

JavaScript 中字符串的 length 属性并不总是“数对”字符个数,尤其遇到 emoji、中文、带变体符号的字母(如带声调的拉丁字母)、代理对(surrogate pairs)等特殊字符时,容易产生意外结果。
length 统计的是 UTF-16 编码单元,不是 Unicode 字符
JavaScript 字符串内部以 UTF-16 编码存储。大多数常用字符(如 ASCII 字母、数字、常见汉字)用 1 个 UTF-16 编码单元(即 2 字节)表示,length 返回值与直观字符数一致。但部分字符(如大部分 emoji、某些古文字、带组合符的字符)需要 2 个连续的编码单元(称为“代理对”)才能表示,此时 length 会返回 2,尽管它在视觉上只是一个字符。
-
'a'.length→ 1 -
'?'.length→ 2(U+1F600 是一个四字节 UTF-8 字符,在 UTF-16 中拆为两个代理单元) -
'??'.length→ 7(由多个 Unicode 标量值 + 零宽连接符组成,含多个代理对和组合符) -
'café'.length→ 5(如果 é 是单独的 U+00E9,则占 1 单元;若写作c a f e \u0301(e + 组合尖音符),则长度为 5,但显示仍为 4 个视觉字符)
如何准确获取“视觉字符数”或“Unicode 码点数”
要得到更符合人类直觉的字符数量(即 Unicode 码点数量),可使用扩展运算符配合 Array.from 或直接用 String.prototype.codePointAt() 遍历:
-
Array.from('?').length→ 1(正确反映码点数) -
[...'??'].length→ 1(ES2015+ 支持,能正确处理代理对和组合序列) - 手动遍历:
let count = 0; for (const _ of '?') count++; // count === 1
注意:这些方法仍不完全等价于“用户看到的字形数量”(grapheme clusters),例如带肤色修饰符的 emoji('??')或 ZWJ 连接序列('?❤️??')在视觉上是一个整体,但目前 JS 原生不提供稳定可靠的图形单元计数 API;如需精确图形单元长度,需借助 Intl.Segmenter(现代浏览器支持):
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });-
[...seg.segment('???')].length→ 1(真正按“人眼所见字形”计数)
常见误用场景与建议
在截断字符串、校验输入长度(如昵称、评论)、分页展示等业务逻辑中,直接依赖 length 可能导致:
- 截断后出现乱码(如只取前 5 个 UTF-16 单元,把代理对切开)
- 用户输入一个 emoji 被算作 2 个字符,违反“最多 10 字”的限制
- 后端用 UTF-8 字节数或 Unicode 码点校验,前后端长度不一致
建议:
- 若需语义化长度(如表单限制),优先用
[...str].length替代str.length - 截断字符串时,用
Array.from(str).slice(0, n).join(''),避免代理对断裂 - 涉及国际化强的场景(如社交 App),考虑使用
Intl.Segmenter处理图形单元 - 与后端约定统一计数方式(推荐:Unicode 码点数),并在文档中明确说明
不复杂但容易忽略——字符串长度不是纯“字符个数”,而是底层编码视角的计量结果。理解 UTF-16 和 Unicode 的关系,是写出健壮文本处理逻辑的第一步。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











