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

总结

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

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

相关推荐
a cool fish(无名)5 分钟前
rust-模块树中引用项的路径
java·前端·rust
前端进阶者15 分钟前
天地图Marker跳一跳动画
前端
火柴就是我19 分钟前
每日见闻之Three.js 根据官方demo 理解相机位置
前端
JosieBook28 分钟前
【web应用】基于Vue3和Spring Boot的课程管理前后端数据交互过程
前端·spring boot·交互
刘大猫.35 分钟前
npm ERR! cb() never called!
前端·npm·node.js·npm install·npmm err·never called
咔咔一顿操作39 分钟前
常见问题三
前端·javascript·vue.js·前端框架
前端程序媛Ying40 分钟前
点击按钮滚动到底功能vue的v-on:scroll运用
javascript
上单带刀不带妹40 分钟前
Web Worker:解锁浏览器多线程,提升前端性能与体验
前端·js·web worke
电商API大数据接口开发Cris1 小时前
Node.js + TypeScript 开发健壮的淘宝商品 API SDK
前端·数据挖掘·api
还要啥名字1 小时前
基于elpis下 DSL有感
前端