String.prototype.length 和 charCodeAt() 不可靠,因 JavaScript 使用 UTF-16 编码,Emoji 等补充平面字符需代理对(两个代码单元)表示,导致 length 返回 2、charCodeAt() 返回代理值而非真实码点。
emoji 和某些特殊字符(如部分中文生僻字、数学符号、组合表情)在 javascript 中可能占用多个 utf-16 代码单元(即两个 char),但只对应一个 unicode 码点(code point)。直接用 string.prototype.length 或 charcodeat() 会误统计为多个“字符”,导致长度、索引、截取等操作出错。而 string.prototype.codepointat() 能正确返回字符的 unicode 码点,是处理这类复杂字符的关键工具。
为什么 length 和 charCodeAt() 不可靠?
JavaScript 字符串基于 UTF-16 编码。大多数常用字符(如 ASCII 字母、基础汉字)用一个 16 位代码单元表示(称为 BMP 字符),length 和 charCodeAt() 表现正常。但 Emoji(如 ?、??)、补充平面字符(U+10000 及以上)或带修饰符的组合序列(如 ???)需用两个代码单元(代理对,surrogate pair)表示,此时:
-
'?'.length === 2(实际应为 1 个字符) -
'?'.charCodeAt(0)返回高位代理(如0xD83C),不是真实码点 for (let i = 0; i 会把一个 Emoji 当作两个“位置”遍历
用 codePointAt() 获取真实码点并统计字符数
codePointAt(index) 在遇到代理对时自动合并高低位,返回完整的 Unicode 码点(整数)。配合循环跳过代理对,就能准确遍历每个字符:
for…of
- 获取第
n个码点对应的字符:[...str][n]或Array.from(str)[n] - 截取前
n个码点:[...str].slice(0, n).join('') - 查找某个码点的位置:用
for (const [i, ch] of [...str].entries())遍历
例如:[...'Hello ?! ??'].slice(0, 8).join('') 精确得到前 8 个视觉字符,不会在 Emoji 中间截断。
注意组合 Emoji 和变体序列
像 '???' 实际由多个 Unicode 码点组成(人 + 皮肤色修饰符 + 零宽连接符 ZWJ + 电脑),codePointAt() 仍能逐个返回这些码点,但语义上它们构成一个“字形”。若需按用户感知的“图形字符”计数(如输入法中一次删除一个贴纸),需额外识别 ZWJ 序列、修饰符等,这时建议使用专门库如 grapheme-splitter 或 Intl.Segmenter(现代浏览器支持):
const segmenter = new Intl.Segmenter('zh', { granularity: 'grapheme' });
const segments = [...segmenter.segment('????❤️??')];
console.log(segments.length); // → 2(按视觉字形)
真正需要精确处理 Emoji 的场景,优先用扩展运算符或 for…of;要求极致语义分词时,再引入 Intl.Segmenter。











