在 JS 中,字符串的 length 不一定等于字符数,也不等于码点数,更不等于"视觉字符数"------只有 Intl.Segmenter 才能准确统计"你看到几个字"。这篇文章适合在你处理微博、输入框、富文本场景下精准统计字符时阅读
习惯不总靠谱
经验或者习惯是个好东西,是工程师锤炼技能、到达炉火纯青的重要助推剂。
但在软件开发行业,仅靠经验还不够。熟练 + 理解原理,才是从菜鸟成长为大神的核心路径。
这不,我就被深深上了一课。
项目要实现一个统计字符数量的功能。这个我熟啊,信手拈来:
javascript
const str = '字符内容';
console.log(str.length);//打印出 str包含的字符数量
写完,我自信满满,自己测试一下,果然通过,就上线发布了。
结果,半夜接到老板电话:字数统计错误。还特意交代了一句:统计数量大于实际数量。
你看,程序员都是火急火燎的,赶紧打开电脑,一通排查后,发现了问题。
javascript
const str = '❤️☺️爱你';
console.log(str.length); // 输出 6
是的,length 返回的是 6 ,而不是用户眼中看到的 4 个字符"。
当现状与原来的思维习惯方式产生冲突时,就是要调整自己的知识认知的时候了
为何 length 无法准确统计字符数?
首先我们要先解释并理解一个东西:Unicode。
Unicode常识
Unicode 是一个标准,定义了世界所有字符以及对应的编号(又称码点),如:
- 'A' = U+0041
- '中' = U+4E2D
- '😀' = U+1F600
U+ 表示 Unicode,后面的值是十六进制。
总结一句话:Unicode 负责编号,不负责存储。
字符的储存
计算机一直有个永恒的问题是绕不开:数据的存储和传输。
直接存储编号可以吗?答案是不行的,因为不同的字符可能相差很大。假设一个编号是1,另一个可能是几千到几万,而计算机内存是结构化的,无法直接存储字面量,而是需要一种编码标准来对字符的编号或者码点进行编码,获得一个结构相对 稳定的形态来存储到内存中。传输,也是同样的道理,通过编码来表示该字符。
一言以蔽之,编码用于:将编码转换成底层存储的字节序列来存储。常见的编码标准有 utf-8、 utf-16 等,由于 js 语言使用utf-16,因此,我们重点介绍一下它。
utf-16如何编码?
码元是编码中用于实际表示字符的最小存储单元。
上文讲过,计算机需要一种编码标准来对字符的编号或者码点进行编码,获得一个结构相对稳定的形态存储。码元很好的诠释了这个问题。
因此,每字符的存储对应一个或多个码元。
utf-16将16位定义一个码元,也就是2个字节。
例如:
- "A" → U+0041 → 用一个码元(2 字节)
- "中" → U+4E2D → 一个码元(2 字节)
- "😀" → U+1F600 → 超过 U+FFFF,需要两个码元(4 字节)
问题解释
再回到length为何无法正常返回字符数量的问题上。
length
返回的是字符串包含的 utf16码元数量,有的字符是一个码元,而有的则是多个。
举例:
javascript
"A".length // 1
"中".length // 1
"😀".length // 2
这解释了绝大多数场景在 length 统计中英文字符通常没问题,但一遇到 emoji、特殊字符,就翻车的情况。
第一次改进
查资料,发现可以用Array.from(str).length
来替代:
javascript
Array.from("❤️☺️ 爱你").length; // 更接近"人眼看到的字符数"
Array.from()
能基于 Unicode code point
(码点) 正确迭代字符,返回字符数组。
现在高兴极了,学了新东西,走路都是跳着的。
再次暴漏问题
事实证明,乐极生悲。
老板再次来电,声音有些怒气:字符统计怎么又不对了,还是老问题,赶紧解决。
一阵慌乱后,我开始平静下来:又要学习新知识了。
顺利拿到日志信息,开始排查,发现:
javascript
Array.from("🇨🇳").length; // 2。 竟然是两个字符?
console.log(Array.from("🇨🇳")); // ['🇨', '🇳']
查阅资料后发现:中国国旗 emoji 是两个"区域字母"组合而成的:🇨(U+1F1E8)+ 🇳(U+1F1F3)
❓ 为何不为每个视觉字符单独分配码点?
原因在此:
Unicode 编号是有限资源,emoji等特殊符号很难预测上限,如果每个符号都单独一个码点,你会发现 unicode标准很快就需要更新或重新设计:编号不够用了。
所以 Unicode 倡导:视觉新字符尽可能通过组合现有字 符 + 零宽连接符(ZWJ) 等方式,而不新增编码。
这下明白了:一个"人眼看到的字符"不一定是一个码点,可能由多个码点组成的。
终极改进版
到底应该如何定义和统计统计"视觉字符数"呢?
答案是用JavaScript 提供的国际化 API:
javascript
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
const result = [...segmenter.segment("👨👩👧👦🇨🇳e")];
console.log(result.length); // 3
这个 API 可以按 grapheme cluster(字素簇) 拆分,也就是视觉上最小的可识别单位来拆分,契合了我们统计"视觉字符"的需求。
总结
我终于松了口气,抬头望了望,天空的月亮。此刻内心竟平静如水,波澜不惊,我有点吃惊于自己的变化。
或许,这才是求知者该有的姿态:不破不立,毕竟,程序员的世界,从来都不止是写代。