表情符号 (Emoji)
Unicode 中收录的图形符号,用于在文本通信中直观地表达情感和概念。
表情符号 (Emoji) 是 Unicode 中收录的图形符号,用于在文本通信中直观地表达情感和概念。它起源于为日本手机制作的符号集,可以确认的最早例子是 1997 年搭载在 J-Phone (后来的 SoftBank) 终端上的 90 种。广为人知的则是 1999 年为 NTT DoCoMo 的 i-mode 制作的 176 种,此后在 2010 年 10 月的 Unicode 6.0 中有 722 种被采纳为国际标准。按 2026 年时点的表情符号规范 (Emoji 17.0) 来数,表情符号共 3,953 个,每次修订都在继续追加。
表情符号的内部结构比外观复杂得多。许多表情符号位于基本多语言平面 (BMP) 之外,在 UTF-16 中要以代理对的形式消耗 2 个编码单元。此外还有把多个码位组合成 1 个表情符号的多种机制:改变肤色的修饰符 (Emoji Modifier)、指定性别的 ZWJ (零宽连接符) 序列、表示国旗的区域指示符号等等。例如"👨👩👧👦" (家庭) 由 7 个码位组成 (4 个人物 + 3 个 ZWJ),在 UTF-16 中是 11 个编码单元,在 UTF-8 中是 25 个字节。不过并不是"所有表情符号都在 BMP 之外"。✅ (U+2705)、☺ (U+263A) 这类源自符号的表情符号位于 BMP 之内,不构成代理对,在 UTF-16 中只占 1 个编码单元。写对表情符号做特殊处理的代码时,必须以这两个系统混在一起为前提来搭。
不同编码的字节数也不同。在 UTF-8 中,BMP 之外的表情符号每个占 4 个字节,前面提到的源自符号的表情符号占 3 个字节,像肤色修饰符或国旗那样组合 2 个码位的占 8 个字节,含 ZWJ 序列的复合表情符号还要更多。在数据库的 VARCHAR 列中存储表情符号时,MySQL 需要 utf8mb4 编码 (utf8 最多只支持 3 个字节,无法存储表情符号)。不了解这一限制就设计数据库,是保存含表情符号的数据时出错的典型陷阱。
社交媒体的字数限制对表情符号的处理方式因平台而异。X (原 Twitter) 把 1 个表情符号计为 2 个字符。短信则只要含有 1 个表情符号,整条消息就会切换到 Unicode (UCS-2) 编码,每条的上限从 160 个字符减少到 70 个字符。再加上 BMP 之外的表情符号在 UCS-2 中占 2 个单位,所以只排表情符号的话,1 条里能装下的大约是 35 个。这里可以作为实务判断基准的是"追加 1 个表情符号的代价不止 1 个字符",在已经贴着上限的文案里再加表情符号,就会比预想更早触发分条发送 (额外计费)。
编程中处理表情符号需要格外注意。JavaScript 的 .length 属性把代理对计为 2,因此 1 个表情符号可能返回 2 或更大的长度。这里常有说法称"用 Array.from() 就能解决",但这并不准确。Array.from() 只是按码位切分,实际试一下,家庭表情符号会得到 7,而指定了肤色的"👍🏽"和日本国旗"🇯🇵"都会得到 2。能拿到与外观一致的 1 的是按书写素簇切分的 Intl.Segmenter,用 new Intl.Segmenter('ja', { granularity: 'grapheme' }) 切分之后再数。把 .length 理解为偏大、把 Array.from() 理解为"只吸收代理对的中间值",就不会在该用哪种数法上判断失误。
从字符计数角度看,表情符号是"看似 1 个字符实为多个字符"的典型代表。根据计数方式 (码位数、UTF-16 编码单元数、字节数、书写素簇数) 的不同,结果会有很大差异。在实现字符计数工具时,妥善处理用户期望的"视觉上的 1 个字符"与内部表示之间的差距至关重要。