在JavaScript的字符串处理场景中,包含特殊表情符号的字符串经常会出现截取异常、长度计算错误的问题,这背后和Unicode的编码规则以及JavaScript的字符串存储方式有关,String.codePointAt()方法就是解决这类问题的核心工具。

Unicode编码与JavaScript字符串的基础关系
Unicode编码将字符分为两个平面:基本多文种平面(BMP)和辅助平面。BMP内的字符码点范围是U+0000到U+FFFF,JavaScript用单个16位UTF-16编码单元存储,比如普通的英文字母、汉字都属于这个范畴。而辅助平面的字符码点范围是U+10000到U+10FFFF,比如大部分表情符号、部分生僻汉字,这类字符需要用两个16位UTF-16编码单元存储,也就是我们常说的代理对。
JavaScript的字符串本质上是一个UTF-16编码单元的序列,所以当我们用传统的charAt()、length属性处理辅助平面字符时,就会把代理对拆成两个独立的单元处理,导致结果错误。
charAt与codePointAt的核心差异
我们先通过对比两个方法的表现,来看codePointAt的优势:
charAt(index):返回指定索引位置的UTF-16编码单元,对于辅助平面字符,只能返回代理对的前半部分或者后半部分,无法得到完整字符。codePointAt(index):返回指定索引位置字符的完整Unicode码点,如果索引位置是代理对的前半部分,会直接返回整个辅助平面字符的码点;如果索引位置是代理对的后半部分,会返回后半部分的码点。
简单示例对比
我们用一个表情符号做测试,比如表情😀的码点是U+1F600,属于辅助平面字符:
// 定义包含表情的字符串 const str = "😀a"; // 查看字符串的length,代理对占两个单元,所以长度是3 console.log(str.length); // 输出3 // 用charAt获取第一个字符,只能拿到代理对的前半部分 console.log(str.charAt(0)); // 输出乱码,因为只拿到了半个字符 // 用codePointAt获取第一个字符的码点 console.log(str.codePointAt(0)); // 输出128512,对应U+1F600,也就是😀的完整码点 // 用codePointAt获取第二个字符的码点,第二个索引是代理对的后半部分 console.log(str.codePointAt(1)); // 输出56832,是代理对后半部分的码点
用codePointAt正确处理表情符号的常用场景
场景1:正确计算包含表情的字符串长度
传统的str.length会把表情符号算成2个字符,我们可以通过遍历码点的方式计算真实长度:
function getRealLength(str) {
let length = 0;
for (let i = 0; i < str.length; i++) {
const code = str.codePointAt(i);
// 辅助平面字符的码点大于0xFFFF,占两个UTF-16单元,索引需要多跳一位
if (code > 0xFFFF) {
length++;
i++;
} else {
length++;
}
}
return length;
}
const testStr = "你好😀世界";
console.log(testStr.length); // 输出6,传统长度错误
console.log(getRealLength(testStr)); // 输出5,真实长度正确
场景2:正确截取包含表情的字符串
截取字符串时如果直接按索引切,可能会把表情符号的代理对拆开,我们可以用codePointAt判断字符范围,实现安全截取:
function safeSubstring(str, start, end) {
let realStart = 0;
let realEnd = 0;
let count = 0;
// 先找到真实的起始索引
for (let i = 0; i < str.length && count < start; i++) {
const code = str.codePointAt(i);
if (code > 0xFFFF) {
i++;
}
count++;
realStart = i + 1;
}
count = 0;
// 再找到真实的结束索引
for (let i = realStart; i < str.length && count < (end - start); i++) {
const code = str.codePointAt(i);
if (code > 0xFFFF) {
i++;
}
count++;
realEnd = i + 1;
}
return str.slice(realStart, realEnd);
}
const subStr = "测试😀表情截取";
console.log(safeSubstring(subStr, 0, 3)); // 输出测试😀,截取正确
console.log(subStr.substring(0, 3)); // 输出测试�,截取错误,拆了代理对
场景3:遍历包含表情的字符串
遍历字符串时如果需要拿到完整字符,可以结合codePointAt和String.fromCodePoint()方法:
function iterateStr(str) {
const result = [];
for (let i = 0; i < str.length; i++) {
const code = str.codePointAt(i);
if (code > 0xFFFF) {
// 辅助平面字符,拼接完整字符
result.push(String.fromCodePoint(code));
i++; // 跳过代理对的后半部分
} else {
result.push(String.fromCodePoint(code));
}
}
return result;
}
const iterateTest = "😀abc😁";
console.log(iterateStr(iterateTest)); // 输出["😀","a","b","c","😁"],每个元素都是完整字符
注意事项
- 调用
codePointAt(index)时,如果index超出了字符串的长度,会返回undefined。 - 如果index位置是代理对的后半部分,返回的码点会落在0xDC00到0xDFFF之间,属于代理对专用范围,此时这个码点没有对应独立字符,需要结合前半部分处理。
- 码点转字符的时候要用
String.fromCodePoint(),不要用String.fromCharCode(),后者无法处理辅助平面的码点。
处理Unicode字符时,只要涉及辅助平面的内容,优先使用codePointAt相关的方法,避免用传统的charAt、length、substring直接操作,就能避免大部分表情符号处理的问题。
String_codePointAtUnicode表情符号码点修改时间:2026-07-07 19:21:35