type
status
date
slug
summary
tags
category
icon
password
引言:令人困惑的
.length在 JavaScript 开发中,处理字符串长度似乎是件简单的事。但你是否遇到过这样的情况:
为什么有些看起来只是“一个”字符的
.length 却是 2?这背后其实与 JavaScript 字符串的底层编码方式——UTF-16 息息相关。核心原因:JavaScript 与 UTF-16 编码
根据 ECMAScript 规范,JavaScript 字符串内部采用 UTF-16 编码存储。理解 UTF-16 是解开
.length 之谜的关键。UTF-16 的特点:
- 基本单位: 它使用 16 位(2 字节)的码元 (Code Unit) 作为基本单位。
- 变长表示:
- 对于 Unicode 基本多文种平面 (Basic Multilingual Plane, BMP) 中的字符 (码点范围 U+0000 到 U+FFFF),UTF-16 使用 一个码元 (2 字节) 表示。这包含了绝大多数常用字符(拉丁字母、数字、标点、大多数 CJK 汉字等)。
- 对于 Unicode 补充平面 (Supplementary Planes) 中的字符 (码点范围 U+010000 到 U+10FFFF),UTF-16 使用 两个码元 (4 字节) 来表示。这两个码元被称为 代理对 (Surrogate Pair)。常见的 Emoji 和一些不常用的 CJK 汉字就属于补充平面。
UTF-16 编码逻辑简述
- BMP 字符 (码点 <= U+FFFF): 直接使用其 Unicode 码点对应的 16 位值作为码元。
- 例如:字符 'A' 的码点是 U+0041。在 UTF-16 中就用一个码元
0x0041表示。
(在 JavaScript 中,
\\uXXXX 用于表示一个 UTF-16 码元)- 补充平面字符 (码点 > U+FFFF): 需要通过特定算法将其码点拆分成一个高位代理码元 (范围 U+D800 到 U+DBFF) 和一个低位代理码元 (范围 U+DC00 到 U+DFFF) 组成的代理对。
- 例如:字符 '💩' (PILE OF POO) 的码点是 U+1F4A9。计算后得到代理对:高位
0xD83D和低位0xDCA9。 - 同样,'𠮷' 的码点是 U+20BB7,也会被编码成一个代理对,因此
.length为 2。
揭晓答案:
.length 统计的是什么?现在答案清晰了:JavaScript 字符串的
.length 属性返回的是字符串所包含的 UTF-16 码元的数量,而不是我们通常理解的“字符”数量。- 对于 BMP 字符,一个字符对应一个码元,
.length符合直觉。
- 对于补充平面字符,一个字符对应两个码元(一个代理对),
.length就会是 2。
历史背景: JavaScript 最初设计时采用了 USC-2 编码(UTF-16 的前身),认为 16 位(65536 个码位)足以覆盖所有字符。后来 Unicode 扩展引入了补充平面,UTF-16 通过代理对机制兼容了这些新字符,但 .length 属性的行为保留了统计码元的原始定义。
用户体验问题与解决方案
这种不一致性在需要精确计算用户可见字符数的场景下(如表单输入长度校验)会造成困扰。用户输入一个 '𠮷',视觉上是一个字符,但程序可能按长度 2 计算。
如何正确计算 Unicode 字符数量?
1. (过时方法参考) 正则替换:
一些库(如旧版
async-validator)曾使用正则表达式将代理对替换为单个占位符来计算长度:这种方法能解决特定问题,但不够通用和优雅。
2. (推荐) ES6+ 现代方法:
ES6 及之后版本提供了更好的 Unicode 支持,推荐使用以下方法:
- 展开语法 (Spread Syntax): 这是最简洁直观的方式。
展开语法或
Array.from会按照 Unicode 字符(码点)来迭代字符串。
ES6+ 对 Unicode 的增强支持
.length 只是冰山一角。早期 JavaScript 对补充平面字符的处理在许多方面都存在问题。ES6 带来了显著改进:- 迭代 (
for...of):for...of循环能正确遍历 Unicode 字符,而传统的for循环会按码元遍历,导致补充平面字符被拆分。
- 字符串方法: 像
slice,substring,split('')等传统方法可能在代理对边界处错误地分割字符。虽然 ES6 没有直接修改这些方法的行为以保持兼容性,但结合Array.from或展开语法可以先转换为字符数组再操作。
- 正则表达式
u标志: ES6 引入了u(Unicode) 标志,使正则表达式能正确处理四个字节的 UTF-16 编码。
u 标志还能正确处理 \\u{XXXXX} 形式的 Unicode 转义。codePointAt()vscharCodeAt():charCodeAt(i)返回指定索引i处的 UTF-16 码元 值 (0-65535)。对于补充平面字符,它只能获取代理对中的第一个码元值。codePointAt(i)返回从指定索引i开始的完整 Unicode 码点 值。它能正确识别代理对并返回补充平面字符的实际码点。
String.prototype.normalize(): 用于处理 Unicode 规范化问题。有些字符可以用多种码元序列表示(例如 'é' 可以是单个字符\\u00E9,也可以是 'e' + 组合重音符e\\u0301)。虽然它们看起来一样,但直接比较 (===) 会是false。normalize()可以将字符串转换为统一的规范形式(NFC 或 NFD),使得视觉和语义上相同的字符能够正确比较。
总结
理解 JavaScript 字符串的 UTF-16 编码和
.length 统计码元的行为至关重要。当需要处理用户可见字符或进行精确的 Unicode 操作时,务必利用 ES6+ 提供的 for...of、展开语法、u 标志、codePointAt() 和 normalize() 等现代特性,以避免潜在的错误和困惑。- 作者:90_blog
- 链接:https://blog.tri7e.com/article/js_string
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
