JavaScript字符串底层基于UTF-16编码,但按码元而非码点计数,导致length、charAt等方法对代理对(如“?”)返回2而非1;应使用for-of、codePointAt、Array.from或u标志正则确保正确处理Unicode字符。

JavaScript 的 String 类型底层基于 UTF-16 编码,但处理方式与标准 UTF-16 有关键差异——它把每个 UTF-16 码元(16 位)当作一个“字符单位”,而不是按完整 Unicode 字符(code point)计数。这直接影响长度计算、索引访问和字符遍历的准确性。
length 属性反映的是 UTF-16 码元个数,不是真实字符数
遇到 emoji 或生僻汉字(如 “?”、“??”)时,str.length 可能大于人眼感知的字符数:
- “?” 的 Unicode 码点是
0x20BB7,需用两个 UTF-16 码元(代理对:0xD842 0xDFB7)表示 →"?".length === 2 - “??” 是多个 Unicode 字符组合的序列(含 ZWJ 连接符),长度可能为 4 或更多
- 空字符串
""长度为 0;含一个普通 ASCII 字符(如"a")长度为 1
安全获取单个字符:优先用 for…of 或 codePointAt()
传统 charAt(i) 或 str[i] 按码元索引取值,对代理对会拆开返回空或乱码:
-
"?"[0]返回""(实际是高位代理项\uD842,无法单独显示) -
"?".charAt(0)同样只取第一个码元,结果不可靠 - 正确方式:
for (const ch of "???") { console.log(ch); }—— 自动按 Unicode 字符迭代 - 或用
str.codePointAt(i)获取指定位置的完整码点(支持 >0xFFFF 的字符),再配合String.fromCodePoint()还原
截取与拼接:注意方法对代理对的兼容性
不同截取方法对负数、越界、代理对的处理逻辑不同:
立即学习“Java免费学习笔记(深入)”;
-
slice(start, end):支持负索引(slice(-2)取末尾两个码元),不交换参数顺序,越界静默处理,能保持代理对完整性 -
substring(start, end):自动交换大小顺序(substring(5,2) === substring(2,5)),负数转为 0,可能切断代理对 -
substr(start, length):已废弃,不应在新代码中使用 - 拼接推荐用
+或模板字面量;concat()功能等价但无优势
Unicode 规范化与比较:避免因编码形式不同导致误判
同一个字符可能有多种合法 UTF-16 表示(如带重音的字母 “Ǒ” 可写作合成形式 \u01D1 或分解形式 \u004F\u030C):
- 直接用
===比较可能返回false,即使视觉相同 - 统一用
str.normalize("NFC")(默认)或str.normalize("NFD")标准化后再比较 - 搜索、排序、去重等场景建议先 normalize,尤其处理用户输入或国际化文本时


















