最后更新:
表情符号的组合改变含义 - 1 个字符与 2 个以上字符传递的信息量差异
当你从手机的表情键盘选择"👨👩👧👦"时,你以为自己输入了 1 个表情符号。然而这个家庭表情的真面目是"👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦"这 7 个码位的连接。外观是 1 个字符,内部是 7 个。在表情符号的世界里,组合方式不同,1 个字符的含义和字符数都会发生巨大变化。
ZWJ 序列 - 用看不见的胶水合并表情符号
ZWJ(Zero Width Joiner)是分配给 Unicode 码位 U+200D 的"零宽度连接符"。它在屏幕上不显示任何内容,但起着将前后表情符号粘合为一体的胶水作用。
机制很简单。将表情 A + ZWJ + 表情 B 排列在一起,如果操作系统或应用有对应这个组合的字形(渲染图像),A 和 B 就会合并显示为一个表情符号。如果没有对应的字形,A 和 B 就原样并排显示。也就是说,ZWJ 序列是一种"能合就合,不能合就照原样"的灵活机制。
| ZWJ 序列 | 组成要素 | 码位数 | 显示 |
|---|---|---|---|
| 👨👩👧👦 | 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 | 7 | 家庭(父母女儿儿子) |
| 👩💻 | 👩 + ZWJ + 💻 | 3 | 女性技术人员 |
| 🏳️🌈 | 🏳️ + ZWJ + 🌈 | 4 | 彩虹旗 |
| 👨🍳 | 👨 + ZWJ + 🍳 | 3 | 男性厨师 |
| 🧑🚀 | 🧑 + ZWJ + 🚀 | 3 | 宇航员 |
| ❤️🔥 | ❤️ + ZWJ + 🔥 | 4 | 燃烧的心 |
| 👩❤️👨 | 👩 + ZWJ + ❤ + VS16 + ZWJ + 👨 | 6 | 情侣 |
家庭表情是 ZWJ 序列中码位数最多的类别之一。4 人家庭的"👨👩👧👦"有 7 个码位,用 UTF-8 编码后达 25 字节。仅仅 1 个表情符号就消耗了相当于 25 个英文字母的数据量。
ZWJ 序列有趣的地方在于,理论上可以尝试连接任意两个表情符号。输入"🐱 + ZWJ + 🐉"(猫和龙)这样未定义的组合也不会报错。由于没有对应的字形,猫和龙只是并排显示而已。Unicode 作为 RGI(Recommended for General Interchange,推荐用于一般交换)正式登记的 ZWJ 序列,在 Emoji 17.0 的一览中为 1,614 件。不过这个数字把每一种肤色变体都算作独立的一件,把肤色变体合并后就只剩 249 件。同一个"到底有多少种"的问题,答案会因数法不同而相差六倍以上,这正与数表情符号本身的难处如出一辙。在这份一览之外,厂商有时也会自行添加支持。
国旗表情 - 2 个地区指示符绘制 1 面旗帜
🇯🇵(日本国旗)看起来是 1 个表情符号,实际上是"地区指示符号字母 J"(U+1F1EF)和"地区指示符号字母 P"(U+1F1F5)这 2 个字符的组合。用专用的 Unicode 字符表示 ISO 3166-1 alpha-2 国家代码"JP"。
| 国旗 | 国家代码 | 地区指示符 | 码位 |
|---|---|---|---|
| 🇯🇵 | JP | 🇯 + 🇵 | U+1F1EF U+1F1F5 |
| 🇺🇸 | US | 🇺 + 🇸 | U+1F1FA U+1F1F8 |
| 🇬🇧 | GB | 🇬 + 🇧 | U+1F1EC U+1F1E7 |
| 🇫🇷 | FR | 🇫 + 🇷 | U+1F1EB U+1F1F7 |
| 🇧🇷 | BR | 🇧 + 🇷 | U+1F1E7 U+1F1F7 |
| 🇰🇷 | KR | 🇰 + 🇷 | U+1F1F0 U+1F1F7 |
地区指示符号从 A 到 Z 共 26 个,理论上可以有 26 × 26 = 676 种组合。但实际显示为国旗的只有 ISO 3166-1 中注册的约 250 个国家和地区代码。未注册的组合(如"🇽🇽")在不同平台上可能显示为"XX"文本或空白。
这种设计蕴含着政治考量。Unicode 联盟为了避免"哪些地区算作国家"这一政治判断,没有直接定义国旗表情,而是委托给了现有的国际标准 ISO 3166-1。如果新国家独立并在 ISO 3166-1 中注册,无需修改 Unicode 规范就能自动使用该国旗表情。
正如URL 字符数限制中提到的,国家代码在互联网的各种场景中都有使用。ccTLD(国家代码顶级域名)的".jp"也基于同一个 ISO 3166-1 国家代码。
肤色修饰符 - 1 个表情变成 5 种颜色的机制
2015 年 Unicode 8.0 引入的肤色修饰符(Emoji Modifier)用于改变人物表情的肤色。基于皮肤科使用的 Fitzpatrick 量表,提供了 5 个等级的修饰符。
| 修饰符 | 码位 | Fitzpatrick 分类 | 示例(👋 + 修饰符) |
|---|---|---|---|
| 🏻 | U+1F3FB | Type I-II(浅肤色) | 👋🏻 |
| 🏼 | U+1F3FC | Type III(较浅肤色) | 👋🏼 |
| 🏽 | U+1F3FD | Type IV(中等肤色) | 👋🏽 |
| 🏾 | U+1F3FE | Type V(较深肤色) | 👋🏾 |
| 🏿 | U+1F3FF | Type VI(深肤色) | 👋🏿 |
添加肤色修饰符后,1 个表情变成 2 个码位。"👋"(U+1F44B)是 1 个码位,而"👋🏽"是"U+1F44B U+1F3FD"的 2 个码位。UTF-8 下为 4 字节 + 4 字节 = 8 字节。仅指定肤色就使数据量翻倍。
当 ZWJ 序列与肤色修饰符组合时,码位数会爆炸式增长。例如肤色不同的情侣表情"👩🏻❤️👨🏿"由 👩 + 🏻 + ZWJ + ❤ + VS16 + ZWJ + 👨 + 🏿 组成,达 8 个码位。以 UTF-8 编码为 28 字节。英文"Hi there!"是 9 字节,也就是说仅 1 个表情就背负着短问候语三倍以上的数据量。
表情俚语 - 组合产生的隐语世界
表情符号不仅有官方含义,在用户社区中也被当作独特的俚语使用。单独看无害的表情符号,组合后可能产生完全不同的含义。
最著名的大概是 🍑🍆 的组合。桃子和茄子这两个食物表情,在社交媒体上被广泛认知为性暗示。麻烦的是,从官方定义看它们始终只是食物。发送方可能只是把一种水果和一种蔬菜并排放在一起,而接收方读到的却是俚语。官方含义与实际被读取的方式之间的这道缝隙,正是表情符号沟通失误的特色所在。
| 表情组合 | 字符数 | 官方含义 | 俚语含义 |
|---|---|---|---|
| 🍑🍆 | 2 字符 | 桃子和茄子 | 性暗示 |
| 🧢 | 1 字符 | 棒球帽 | 谎言(cap = 说谎) |
| 💀 | 1 字符 | 骷髅 | 笑死了 |
| 🐐 | 1 字符 | 山羊 | GOAT(Greatest Of All Time) |
| 👁️👄👁️ | 3 字符 | 眼睛和嘴巴 | 震惊、困惑的表情 |
| 🫠 | 1 字符 | 融化的脸 | 尴尬、害羞 |
| 🤡 | 1 字符 | 小丑 | 做了蠢事的人 |
"👁️👄👁️"是用 3 个表情排列成脸的"表情艺术":眼 + 嘴 + 眼,表达"难以言喻的表情"。这个 3 字符组合在社交媒体上扩散开来,用于表达震惊、困惑或"看到了不该看的东西"的意味。有趣的是,它与 ZWJ 序列不同,完全没有使用任何连接机制,仅仅依靠"3 个表情并排就会被读成一张脸"这一点成立。
表情俚语因世代和地区而异。在日本,🙏 用于表示"拜托"或"谢谢",而在欧美有时被解读为"击掌"。正如表情符号的 Unicode 与字符数计算中所述,表情符号的技术字符数与人类感受到的"含义量"完全是不同维度的事情。
计数的单位不同,字符数就会改变
表情符号的字符数之所以在不同的地方得出不同的结果,原因并不是各平台随心所欲,而是"把什么当作 1 个单位"这一点不同。同样是"👨👩👧👦",只要更换单位,答案就会在 1 到 25 之间移动。
| 计数单位 | 👨👩👧👦 | 🇯🇵 | 使用该单位的场合 |
|---|---|---|---|
| 字素簇(视觉上的 1 个字符) | 1 | 1 | Swift 的 count、Intl.Segmenter |
| 码位 | 7 | 2 | Python 3 的 len()、[...str].length |
| UTF-16 代码单元(16 位单位) | 11 | 4 | JavaScript 的 .length、UCS-2 模式的 SMS |
| UTF-8 字节 | 25 | 8 | Rust 的 len()、Go 的 len()、数据库的字节长度 |
| X(旧 Twitter)已公开的发帖加权 | 2 | 2 | X 的 280 字符限制 |
发布界面上的剩余字符计数器究竟按哪个单位递减,除了服务方主动公开之外,从外部是无法确定的。如果想给已经接近上限的帖子添上表情符号,可靠的做法是先粘贴进去,观察计数器的实际反应。X(旧 Twitter)是少数已公开计数规则的例子。
Twitter(X)比较宽容,ZWJ 序列的家庭表情和国旗表情都按视觉上的 1 个表情处理(但内部按 2 字符计算权重)。正如Twitter 的字符数限制中详细介绍的,在 280 字符限制内,每个表情按 2 字符计算。
而 JavaScript 的 .length 属性返回 UTF-16 代码单元数,因此包含代理对的表情会返回比视觉字符数更大的值。家庭表情"👨👩👧👦"的 .length 为 11。要获得准确的字符数,可以使用 Array.from(str).length 或 [...str].length,但这些方法也会分解 ZWJ 序列返回 7。要按字素簇(grapheme cluster)计数,需要使用 Intl.Segmenter API。
也可以参考SNS 字符数限制汇总。了解各平台的计数方式差异,可以减少发布大量表情时遇到"字符数超限"的困扰。
表情接龙和电影名猜谜 - 游戏中的字符数
用表情符号玩的游戏从字符数角度来看也有有趣的发现。
"表情接龙"是用表情符号的名称玩日语接龙游戏。🍎(苹果)→ 🦍(大猩猩)→ 🍜(拉面)……以此类推。规则简单,但玩的时候总会为名称争执不下。就拿这个 🍜 来说,它的 Unicode 字符名是"STEAMING BOWL"(热气腾腾的碗),并没有任何地方规定它就是拉面。🍛 的字符名是"CURRY AND RICE",读作"咖喱"还是"咖喱饭",会改变下一个能接的表情。不熟悉的表情最容易出问题,🫥 的字符名是"DOTTED LINE FACE"(虚线脸),但日常口语里各人的叫法各不相同。表情符号的各语言名称定义在 Unicode 的 CLDR(Common Locale Data Repository)中,这套本地化名称与 Unicode 字符名是两套不同的登记体系,因此事先约定以 CLDR 名称为准,才能让游戏不至于中途散场。
"用表情猜电影名"也很受欢迎。例如"🦁👑"是"狮子王"(2 个字符表达 3 个字的标题),"👻👻👻🔫"是"捉鬼敢死队"(4 个字符表达 5 个字的标题),"🧙♂️💍🌋"是"指环王"(3 个字符表达 3 个字的标题)。表情符号的组合可以说是一种大幅压缩字符数同时传递含义的"超压缩语言"。不过被压缩的只是看得见的字符数,"🧙♂️"在内部是由 4 个码位构成的 ZWJ 序列。若以数据量来衡量,表情符号毫无疑问站在膨胀的那一侧。
在编程中获取表情的"真实字符数"
对开发者来说,表情符号的字符数计算是个头疼的问题,因为不同语言和运行时返回的值各不相同。
| 语言 / 环境 | 方法 | "👨👩👧👦"的结果 | 计数单位 |
|---|---|---|---|
| JavaScript | "👨👩👧👦".length | 11 | UTF-16 代码单元 |
| JavaScript | [..."👨👩👧👦"].length | 7 | 码位 |
| Python 3 | len("👨👩👧👦") | 7 | 码位 |
| Swift | "👨👩👧👦".count | 1 | 字素簇 |
| Rust | "👨👩👧👦".len() | 25 | 字节(UTF-8) |
| Go | len("👨👩👧👦") | 25 | 字节(UTF-8) |
只有 Swift 返回"1",因为 Swift 采用字素簇(grapheme cluster)作为字符单位。这是最接近人类直觉的结果,但内部处理成本较高。在 JavaScript 中要获得相同结果,需要使用 Intl.Segmenter。
正如Unicode 基础知识中所述,"字符数"的定义因上下文而异。表情符号的组合将这个问题展现得最为鲜明。与全角和半角的字符数计算差异一样,请记住表情的计数方式也因平台和语言而异。
表情符号的未来 - 无限的组合可能性
在截至 2026 年 8 月的最新版本 Emoji 17.0 的统计表中,表情符号总数为 3,953 件。不过这个数字同样把每一种肤色变体都算作独立的一件,一览中 3,944 件完全限定序列里有 2,030 件(超过半数)是肤色变体。"表情符号一共有多少个"之所以没有唯一答案,是因为贯穿全文的"把什么当作 1 个单位"这一问题,同样出现在总数的统计上。
较新的机制是 Emoji 15.1 追加的"方向"序列。用 ZWJ 把 🏃(跑步的人)与右箭头连接起来,就变成 🏃➡️(向右跑的人)。其内部是 🏃 + ZWJ + ➡ + VS16,共 4 个码位,以 UTF-8 编码为 13 字节。仅仅改变朝向,数据量就变成原本的 🏃(4 字节)的三倍以上。
表情符号的组合大幅提升了文本通信的表达力。日常聊天中不需要在意 1 个表情内部由多少个码位构成。但在有字符数限制的社交媒体发帖、数据库字符数设计、编程中的字符串处理时,"视觉字符数"与"内部字符数"的差距可能成为意想不到的陷阱。
正如LINE 消息字符数一文中提到的,大量使用表情的消息比纯文本消息数据量更大。下次选择表情时,不妨想象一下那 1 个字符背后隐藏着多少个码位。