统计字符数错一半,我被 length 坑了两次

在 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(字素簇) 拆分,也就是视觉上最小的可识别单位来拆分,契合了我们统计"视觉字符"的需求。

总结

我终于松了口气,抬头望了望,天空的月亮。此刻内心竟平静如水,波澜不惊,我有点吃惊于自己的变化。

或许,这才是求知者该有的姿态:不破不立,毕竟,程序员的世界,从来都不止是写代。

相关推荐
小小愿望1 小时前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
小小愿望1 小时前
项目启功需要添加SKIP_PREFLIGHT_CHECK=true该怎么办?
前端
烛阴1 小时前
精简之道:TypeScript 参数属性 (Parameter Properties) 详解
前端·javascript·typescript
海上彼尚2 小时前
使用 npm-run-all2 简化你的 npm 脚本工作流
前端·npm·node.js
开发者小天2 小时前
为什么 /deep/ 现在不推荐使用?
前端·javascript·node.js
如白驹过隙3 小时前
cloudflare缓存配置
前端·缓存
excel3 小时前
JavaScript 异步编程全解析:Promise、Async/Await 与进阶技巧
前端
Jerry说前后端3 小时前
Android 组件封装实践:从解耦到架构演进
android·前端·架构
步行cgn4 小时前
在 HTML 表单中,name 和 value 属性在 GET 和 POST 请求中的对应关系如下:
前端·hive·html
hrrrrb4 小时前
【Java Web 快速入门】十一、Spring Boot 原理
java·前端·spring boot