开始前,会这些就够了
- 能运行一小段
JavaScript,认识字符串即可
遇到不熟悉的词,先看这 6 个解释
- 字节
- 在这里介绍的现代文件和网络格式中,一个字节由八个位组成,常用来计量数据大小。它有二百五十六种位组合,但这些组合代表什么仍由格式规定。
- 字符编码
- 字符编码是把文字与可存储的数据对应起来的规则。写入时按规则产生数据,读取时按相应规则还原文字;双方使用不同规则,就可能看到乱码或替代符号。
- Unicode
- 它是一套统一处理世界文字和符号的标准,为字符分配编号,并规定相关属性与处理规则。程序因此能交换多种语言的文本,不必为每种语言另建互不相通的编号。
- UTF-8
- 它是把统一字符编号写成字节序列的一种编码方式,长度并不固定。一个可编码的字符编号会使用一到四个字节,所以文字数量与文件字节数通常不能直接画等号。
- 码点
- 码点是字符编号空间中的一个数值位置,可以分配给文字、符号或其他用途。它帮助程序识别文本中的基本成员,但不是屏幕宽度,也不是文件里实际使用的字节数。
- 字素簇
- 字素簇是程序按文字分段规则划出的一组码点,尽量接近读者认为的一个字符。处理光标移动、截取昵称或字数限制时,它通常比逐个编号切开更符合使用直觉。
你在昵称框里只放了一个表情,网页却说占了两个字符;把“女程序员”表情删掉一半,还可能留下一个普通人物。这不一定是输入法坏了,而是界面和程序正在用不同的尺子量同一段文字。
先约定问题:我们是想限制用户看见的字数,还是限制上传的数据大小?两件事都需要计数,但不能共用一个没有解释的“长度”。下面用四把尺子观察,先不背编码表。
一个表情,究竟算几个?
输入一小段文字,比较同一内容的四种计数方式。
- UTF-16 代码单元
- 7
- Unicode 码点
- 4
- 字素簇
- 1
- UTF-8 字节
- 15
码点:U+1F469 · U+1F3FD · U+200D · U+1F4BB
UTF-8 十六进制:F0 9F 91 A9 F0 9F 8F BD E2 80 8D F0 9F 92 BB
这里只在当前页面计算。字素簇近似表示用户看到的一个字符;具体切分受浏览器所用 Unicode 版本影响。UTF-8 字节数不等于模型的 Token 数。
同一段文字,有四种常见量法
码点是 Unicode 给文字元素分配的编号。字母、汉字、组合重音和连接符都可能各有编号。一个看起来完整的符号,未必只用一个码点;例如字母 e 后面再放组合重音,两个编号可以一起显示成一个带重音字母。
JavaScript 的字符串 length 数的是 UTF-16 码元。你可以先把码元理解成这种编码使用的存储单位:常见汉字通常占一个,一些 emoji 占两个。字节则是保存或发送数据时的计量单位;换成 UTF-8 后,同一文字的字节数量又会改变。
把一起显示的部分作为一组
字素簇是更接近用户所说“一个字”的分组方式。一个字母与后面的重音通常属于同一组;某些 emoji 序列通过连接符组成一个整体。字符串展开写法 [...text] 能按码点遍历,却仍会把组合重音、连接符和表情组成部分分别列出来。
Intl.Segmenter 可以按字素簇划分。它比直接用 length 更适合昵称计数、选取前几个可见字符等任务,但不等于测量屏幕宽度:一个字素簇可能很宽,两个字素簇也可能由字体连成一幅字形。字体、语言和运行环境的数据版本仍会影响显示与分段。
先猜结果,再运行四把尺子
代码依次输出文字、码元数、码点数、字素簇数、UTF-8 字节数。第三项输入故意用 e 加组合重音,而不是键盘上可能直接得到的预组合字母。先把自己的预测写在纸上,再看哪些列相同、哪些不同。
遇到看似相同却计数不同的文字,不要立刻删掉“多余字符”。重音和连接符可能正是用户想表达的内容。下面只统计完整字符串,没有演示任意位置截断;按存储单位切开,有可能切断一对码元或一个表情组合。
const segmenter = new Intl.Segmenter("zh", { granularity: "grapheme" });
for (const text of ["A", "😀", "e\u0301", "👩💻"]) {
console.log(JSON.stringify(text), text.length, [...text].length,
[...segmenter.segment(text)].length, new TextEncoder().encode(text).length);
}怎样运行:保存为 count-text.js,用支持 Intl.Segmenter 的 Node.js 运行:node count-text.js。也可在支持该 API 的现代浏览器控制台运行。
运行后对照结果
"A" 1 1 1 1
"😀" 2 1 1 4
"é" 2 2 1 3
"👩💻" 5 3 1 11给限制写上单位,才不会互相打架
假设产品规定昵称最多十二个用户可见字符,同时接口最多接收一百个 UTF-8 字节。这两个限制可以同时存在:前者照顾输入体验,后者限制数据量。界面要说清是哪一项超了,服务端也要采用相同的规则,不能前端按字素簇、后端突然按码元报“字数太多”。
还要区分计数与相等。两段文字看起来一样,不代表底层序列一定相同;统一 Unicode 规范化形式可以解决一部分等价写法,却不能证明两个相似昵称是同一个人。身份识别、搜索和存储各有规则,不能靠一个更聪明的长度函数包办。
记住这三点
- 先写明长度的单位:码元、码点、字素簇或字节。
- 面向用户的字符计数通常优先考虑字素簇;传输大小另算字节。
- 分组数量不等于字体宽度,也不等于身份或字符串相等判断。
别把例子用过头
- [...text].length 数码点,仍可能把一个组合表情算成多个。
- 字素簇是通用分段规则,不是对所有语言和字体的完美视觉测量。
自己试一小步
为一个昵称输入框设计两条能解释清楚的长度规则。
- 自选普通汉字、补充平面汉字、带重音字母和组合 emoji 各一项。
- 用四种方式计数,再用字素簇截取前两个单位;不要用原始下标硬切。
- 写出面向用户的超限提示,并说明前后端要共同约定什么。
做到这些,就算完成
- 每列都写清单位,保留实际输入而不只贴截图。
- 截取结果没有孤立的半个码元,且能解释组合符号为何被保留。
- 区分界面字数限制和数据字节上限,没有把一种计数宣称为万能。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- String.length:UTF-16 码元长度 (新标签页)↗
支持 length 的计数单位、emoji 与码点遍历的区别。
- Intl.Segmenter:文字分段 (新标签页)↗
支持按字素、词和句分段;说明旧浏览器兼容性限制。
- Unicode UAX #29:Text Segmentation (新标签页)↗
支持扩展字素簇、组合字符、语言定制,以及字素簇不等于字形的边界。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
MDN 中文:把文字分成字、词或句 (新标签页)↗
先声明按字素还是词计数,保留分段列表,并和码位计数对照。
查看学习准备与访问说明 →