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 码点(整数)。配合循环跳过代理对,就能准确遍历每个字符:
function countCodePoints(str) {
let count = 0;
for (let i = 0; i < str.length; i++) {
str.codePointAt(i); // 获取当前位置的码点
count++;
// 如果当前是高位代理(0xD800–0xDFFF),且下一位是低位代理,则跳过下一位
if (str.charCodeAt(i) >= 0xD800 && str.charCodeAt(i) <= 0xDFFF) {
i++; // 跳过代理对的第二部分
}
}
return count;
}
更简洁安全的做法是使用 for…of 循环(它天然按码点迭代):
function countCodePoints(str) {
let count = 0;
for (const _ of str) count++;
return count;
}
// 或直接:[...str].length
✅ 这两种方式都正确返回 '???'.length → 2(不是 4 或 5)。
安全地按码点索引和截取字符串
不能直接用 str[i] 或 str.substring(start, end) 做“第 N 个字符”操作,因为 i 是 UTF-16 索引而非码点索引。正确做法:
- 获取第
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。

















