JavaScript 字符
基本多语言平面(BMP)
JavaScript 字符串由 16 位码元(code unit)组成。对多数字符来说,每 16 位码元对应一个字符。换句话说,字符串的 length 属性表示字符串包含多少 16 位码元:
1 2
| let message = "abcde"; console.log(message.length);
|
JavaScript 字符串使用了两种 Unicode 编码混合的策略:UCS-2 和 UTF-16。对于可以采用 16 位编码的字符(U+0000~U+FFFF),这两种编码实际上是一样的。
对于 U+0000~U+FFFF 范围内的字符,length、 charAt()、 charCodeAt() 和 fromCharCode() 返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用 16 位表示的,而这几个方法也都基于 16 位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const messageList = ["a", "b", "c", "d"]; const message = messageList.join(""); const charAtList = []; const charCodeList = [];
for (let i = 0; i < message.length; i++) { charAtList.push(message.charAt(i)); }
for (let i = 0; i < message.length; i++) { charCodeList.push(message.charCodeAt(i)); }
console.log("message length:", message.length); console.log("charAt:", charAtList); console.log("charCodeAt:", charCodeList);
|

代理对
这个对应关系在扩展到 Unicode 增补字符平面时就不成立了。问题很简单,即 16 位只能唯一表示65536 个字符。这对于大多数语言字符集是足够了,在 Unicode 中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个 16 位码元的策略称为代理对。
在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const messageList = ["a", "b", "👨👩👧👦", "c", "d"]; const message = messageList.join(""); const charAtList = []; const charCodeList = [];
for (let i = 0; i < message.length; i++) { charAtList.push(message.charAt(i)); }
for (let i = 0; i < message.length; i++) { charCodeList.push(message.charCodeAt(i)); }
console.log(message.length); console.log("charAt", charAtList); console.log("charCodeAt", charCodeList);
|

这些方法仍然将 16 位码元当作一个字符,事实上索引 2 至 索引 12 对应的码元应该被看成一个代理对,只对应一个字符。
解决方法
Intl.Segmenter
Intl.Segmenter 对象支持语言敏感的文本分割,允许你将一个字符串分割成有意义的片段(字、词、句)。
1 2 3 4 5
| const message = "你好,ab👨👩👧👦cd";
const segmenterFr = new Intl.Segmenter("zh-CN", { granularity: "grapheme" });
const length = Array.from(segmenterFr.segment(message)).length;
|

为了方便使用,可以给字符串扩展一个名为 segmenterLength 的方法,用于计算字符串中包含的字符数量。
1 2 3
| String.prototype.segmenterLength = function () { return Array.from(new Intl.Segmenter("zh-CN", { granularity: "grapheme" }).segment(this)).length; };
|

参考文献
- MDN
- JavaScript 高级程序设计 第 4 版