为什么 “emoji“ 的长度是 11?聊聊字符串里的“长度谎言”

“👨👩👧👦” “👨👩👧👦” “👨👩👧👦” “👨👩👧👦”
🔠 字符串:理想中的“所见即所得”
在初学者眼里,处理文字是最简单的:
| 动作 | 代码行数 (理想状态) | 描述 |
|---|---|---|
| 定义变量 | 1 行 | String name = "张三"; |
| 计算长度 | 1 行 | name.length(); // 返回 2 |
| 存入数据库 | 1 行 | INSERT INTO users VALUES ('张三'); |
| 结果 | - | 一切正常,世界和平。 |
现实是: 你收到了一个叫 “锟斤拷” 的用户,他发了一个 💩 表情,然后你的数据库报错了,你的计算逻辑崩了,你的页面乱码了。
👾 第一关:上古神兽“锟斤拷” (Encoding Hell)
这是每一个中国程序员的童年阴影。
场景:
老系统 A 用的是 GBK 编码(一个汉字占 2 字节)。
新系统 B 用的是 UTF-8 编码(一个汉字占 3 字节)。
恐怖故事:
系统 A 把“张三”传给了系统 B。
系统 B 傻傻地用 UTF-8 的规则去读 GBK 的二进制数据。
结果:
原本的“张三”,变成了 “”(一堆黑色问号),或者变成了 “锟斤拷”(这是因为 UTF-8 的容错字符被再次编码产生的奇观)。
后果:
用户看到满屏的乱码。
修复难度: ⭐⭐⭐⭐⭐。一旦数据以错误的编码存进了数据库,就像把墨水滴进了水里,再也还原不回来了。你只能看着那一堆乱码流泪。
💩 第二关:MySQL 的“弥天大谎” (utf8 vs utf8mb4)
这是 99% 的后端新手都会踩的惊天巨坑。
场景:
你建表时,为了支持中文,把字符集设为了 utf8。
CREATE TABLE users (name VARCHAR(20)) CHARSET=utf8;
你觉得很稳。
恐怖故事:
用户注册,名字叫:“大帅哥😎”。
他输入了一个 Emoji 表情(😎)。
后端报错: Incorrect string value: 'ð' for column 'name'。
甚至更惨: 没有报错,但是名字被截断了,只存进去“大帅哥”,后面的表情丢了。
真相:
MySQL 里的 utf8 不是真正的 UTF-8!
- 真正的 UTF-8 是 4 字节 的(能存 Emoji)。
- MySQL 的
utf8是 3 字节 的(是个阉割版,存不下 Emoji)。 - 你必须使用
utf8mb4。
后果:
如果你的 APP 要做“评论功能”或“聊天功能”,只要你没配 utf8mb4,用户发的每一个表情包都会变成一次报错。
📏 第三关:长度的谎言 (Length)
产品经理说:“用户名限制 10 个字。”
你写了代码:if (name.length() > 10) return Error;
场景:
用户输入了一个“👨👩👧👦”(一家四口)的 Emoji。
看起来是 1 个字,对吧?
恐怖故事:
- 在 Java/JS 里,
"👨👩👧👦".length()等于 11! - 为什么?因为这个 Emoji 实际上是 4 个 Emoji 拼起来的:
- 👨 (男人) + ZWJ (连接符) + 👩 (女人) + ZWJ + 👧 (女孩) + ZWJ + 👦 (男孩)。
- 它是一个组合技。
后果:
用户明明只输了一个表情,你的系统提示:“字数超限”。
或者,你的数据库字段定义了 VARCHAR(10),结果连这“一个字”都存不进去。
🔪 第四关:截断的惨剧 (Truncation)
接上一关。既然 Emoji 这么长,那我截断一下总行了吧?
你想截取前 2 个字符:name.substring(0, 2)。
场景:
名字是 “🐮🍺” (牛啤)。
你截了一半。
恐怖故事:
Emoji 是由 2 个字符(代理对) 组成的。
你如果从中间切开,就相当于把一个汉字劈成了两半。
结果: 变成了 (无效字符)。
更严重的后果: 如果这个半截字符出现在 JSON 结尾,整个 JSON 格式会坏掉,前端解析失败,页面白屏。
👻 第五关:看不见的杀手 (Zero Width Characters)
这是黑客和恶作剧者的最爱。
场景:
零宽字符 (Zero Width Space)。这种字符在屏幕上是看不见的,宽度为 0。
恐怖故事 1:
用户注册名为 admin(里面夹了 5 个零宽字符)。
肉眼看:是 admin。
系统判断:admin != admin(管理员)。注册成功!
后果: 你的系统里出现了两个“真假美猴王”。客服根本分不清谁是谁。
恐怖故事 2:
水印泄密。
公司为了防止员工截图泄密,在内部网页的文字里,悄悄插入了代表员工 ID 的二进制零宽字符。
你肉眼看不见。
当你截图发给竞争对手时,公司通过解码截图里的“隐形文字”,直接定位到是你泄的密。
(好吧,这对公司是好事,对摸鱼的你是恐怖故事。)
💣 第六关:一字杀机 (The Character of Death)
历史上,iOS 和 Android 都出现过**“特定字符崩溃 Bug”**。
场景:
2018 年,iOS 爆出“泰卢固语字符 Bug”。
只要你的 iPhone 收到一条包含某个特定印度语字符的消息。
你的微信/iMessage/WhatsApp 会立刻闪退。
甚至你的手机会无限重启(Respring)。
原因:
操作系统的字体渲染引擎在处理这个复杂的组合字符时,逻辑死循环了。
后果:
那几天,损友们互相发送这个字符。谁点开谁死机。
你对此无能为力,因为这是系统层面的 Bug,你写再多代码也防不住。
💡 结论:文字是世界上最复杂的数据结构
最终,那个理想中“简单的字符串”,变成了:
- 存: 必须用
utf8mb4,防止 Emoji 丢失。 - 算: 必须用
Grapheme Cluster(字素簇)算法来计算长度,而不是简单的.length()。 - 防: 必须过滤零宽字符、控制字符(BOM 头)。
- 转: 必须保证全链路编码一致(UTF-8),防止乱码。
为什么程序员听到“乱码”两个字会本能地颤抖?
因为那是文明的隔阂。
计算机试图用 0 和 1 去承载人类五千年的语言文明(中文、Emoji、藏文、楔形文字…),这本身就是一场西西弗斯的苦役。











