Skip to main content
Best Emojis

开发者的表情符号指南

大多数表情相关的 bug 都源于同一个错位:代码数的东西和读者看到的不一样。一个家庭表情看起来是一张图,却有 7 个码点、11 个 JavaScript 字符和 25 个字节。我们用常见的正则、分段器和长度检查跑遍了 Unicode 官方列表中的全部 3,963 个表情,并在本页公布它们实际匹配到的结果,让你在 bug 报告告诉你选了哪种方法之前,先选对适合场景的那一种。

3,963
个 Unicode 18.0 表情,全部测试
2,770
个由多个码点组成
271
个被 Extended_Pictographic 过滤器漏掉
35
字节:最长表情的大小

同一个表情,每种语言长度都不同

每个单元格是该表达式对列首表情的返回值。结果分为四类:UTF-16 单元、码点、UTF-8 字节,以及字素(人所数的单位)。字素一列由本服务器上的 Intl.Segmenter 计算,因此 Unicode 数据较旧的运行时,对最新表情可能返回更大的值。

表达式计数单位😀❤️👍🏽🇯🇵👩‍💻👨‍👩‍👧‍👦🧑🏻‍❤️‍💋‍🧑🏼
JavaScripts.lengthUTF-16 单元224451115
JavaScript[...s].length码点12223710
JavaScriptIntl.Segmenter字素1111111
Pythonlen(s)码点12223710
Pythonlen(s.encode('utf-8'))UTF-8 字节4688112535
Javas.length()UTF-16 单元224451115
Javas.codePointCount(0, s.length())码点12223710
C#s.LengthUTF-16 单元224451115
C# (.NET 5+)new StringInfo(s).LengthInTextElements字素1111111
Golen(s)UTF-8 字节4688112535
Goutf8.RuneCountInString(s)码点12223710
Rusts.len()UTF-8 字节4688112535
Rusts.chars().count()码点12223710
Swifts.count字素1111111
Swifts.unicodeScalars.count码点12223710
Rubys.length码点12223710
Rubys.grapheme_clusters.length字素1111111
PHPstrlen($s)UTF-8 字节4688112535
PHPmb_strlen($s)码点12223710
PHPgrapheme_strlen($s)字素1111111
MySQLCHAR_LENGTH(s)码点12223710
MySQLLENGTH(s)UTF-8 字节4688112535
SQL ServerLEN(s) on nvarcharUTF-16 单元224451115

简单粗暴的截断会把表情变成什么

下面是对家庭表情 👨‍👩‍👧‍👦 按每个 n 执行 s.slice(0, n) 的结果。大多数截断并不会明显出错,只是返回一个不同的、更小的家庭。这类 bug 就是这样悄无声息地上线的。

在 n 处截断剩下的内容剩余码点
s.slice(0, 1)�半个代理对(显示为 �)0
s.slice(0, 2)👨1
s.slice(0, 3)👨‍2
s.slice(0, 4)👨‍�半个代理对(显示为 �)2
s.slice(0, 5)👨‍👩3
s.slice(0, 6)👨‍👩‍4
s.slice(0, 7)👨‍👩‍�半个代理对(显示为 �)4
s.slice(0, 8)👨‍👩‍👧5
s.slice(0, 9)👨‍👩‍👧‍6
s.slice(0, 10)👨‍👩‍👧‍�半个代理对(显示为 �)6
s.slice(0, 11)👨‍👩‍👧‍👦7

会换国家的国旗

区域指示符字母从字符串开头起两两配对。从 🇺🇸🇯🇵 开头去掉一个码点,剩下的字母就会换一种方式配对:🇸🇯🇵。没有报错,只是变成了另一个国家。

🇺🇸🇯🇵 US + JP → 🇸🇯 SJ (Svalbard & Jan Mayen) + 🇵

哪种表情正则能匹配什么

于 2026-10-20 在 Node 25.2.1(内置 Unicode 17.0 数据)上,针对全部 3,963 个 fully-qualified 表情测试。只有当模式匹配整个表情时才算匹配成功。

模式匹配数漏掉的内容
/^\p{RGI_Emoji}$/v3,944 / 3,963所有比引擎 Unicode 数据更新的表情:Emoji 18.0 (19)
Intl.Segmenter3,963 / 3,963在此运行时上无遗漏
/\p{Extended_Pictographic}/u3,692 / 3,963259 个国旗和 12 个键帽,它们完全不含象形码点
/^\p{Extended_Pictographic}$/u1,193 / 3,963所有由多个码点组成的表情:肤色、国旗、键帽、ZWJ 序列,以及带样式选择符的表情

还有一个要避开的:\p{Emoji} 也会匹配 # * 0 1 2 3 4 5 6 7 8 9 © ®。用它来删除表情,会把文本里的所有数字一并删掉。

删除表情:每种方法会留下什么

下面两行原本是同一段文本。残留字符在大多数字体中不可见,正因如此,它们会在之后的搜索、去重和长度限制中引发问题。

Great 👍🏽 job ❤️ 👩‍💻 🇯🇵 #️⃣1

方法结果残留码点
删除 \p{Extended_Pictographic}Great 🏽 job ️ ‍ 🇯🇵 #️⃣1U+1F3FD U+FE0F U+200D U+1F1EF U+1F1F5 U+FE0F U+20E3
删除 \p{RGI_Emoji}Great job 1无

表情在数据库中占多少空间

全部 3,963 个表情每个所占的 UTF-8 字节数。30% 的表情不超过四个字节;最长的需要 35 字节。如果某列按字节而不是按字符限制长度,请根据这张表来设定大小,而不是看文本的外观。

UTF-8 字节可容纳的表情占比
≤ 41,19330%
≤ 82,34659%
≤ 112,42461%
≤ 173,42486%
≤ 283,86898%
≤ 353,963100%

🧑🏻‍❤️‍💋‍🧑🏼 U+1F9D1 U+1F3FB U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F9D1 U+1F3FC

选择与你所执行限制相匹配的计数方式

不存在唯一正确的长度,只有与你所执行规则相符的那一种。按字节限制的数据库列需要 UTF-8 字节。向用户承诺 50 个字符的文本框需要字素,否则输入三面国旗的用户会被告知用掉了 12 个字符。SQL Server 是个例外:nvarchar(n) 按 UTF-16 单元计数,所以 😀 在 nvarchar 中占两个字符,而在按码点计数的 MySQL 或 PostgreSQL varchar 中只占一个。先确定你指的是四种中的哪一种,把它写进变量名,其余的就顺理成章了。

按字素边界截断,而不是按索引

任何按索引的截断都可能落在表情中间。对 UTF-16 来说,最坏的情况是半个代理对,大多数渲染器会把它显示成带问号的方框,有些 JSON 编码器则会直接拒绝。按码点截断时表面上什么都没坏,但这更糟:家庭少了一个孩子,肤色脱落成一个棕色方块,或者国旗变成两个字母。解决办法是先分段,再截取整数个字素。自 Firefox 125 于 2024 年 4 月支持以来,Intl.Segmenter 已在所有主流浏览器中可用,在 Node 中可用得更早,手写拆分器的老借口已经不成立了。

可直接复制的代码片段

简短,并在语言允许时不依赖任何第三方库。每段代码解决上面表格中的一种情况。

统计读者看到的数量(JavaScript)

const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
const visibleLength = (s) => [...seg.segment(s)].length;

visibleLength('👨‍👩‍👧‍👦 ok'); // 4, while '👨‍👩‍👧‍👦 ok'.length is 14

截断时不拆开表情(JavaScript)

function truncate(s, max, ellipsis = '…') {
  const parts = [...seg.segment(s)].map((x) => x.segment);
  return parts.length <= max ? s : parts.slice(0, max - 1).join('') + ellipsis;
}

找出所有表情,包括国旗和键帽(JavaScript)

// ES2024 'v' flag: Chrome 112, Safari 17, Firefox 116, Node 20
const emoji = /\p{RGI_Emoji}/gv;
'Go 🇯🇵 #️⃣1 👍🏽'.match(emoji); // ['🇯🇵', '#️⃣', '👍🏽']

Python 中的同样做法,使用 regex 模块

import regex  # pip install regex; the built-in re has no \X

graphemes = regex.findall(r'\X', '👨‍👩‍👧‍👦 ok')
len(graphemes)  # 4, while len('👨‍👩‍👧‍👦 ok') is 10

能接受所有表情的列

-- MySQL: utf8mb4 on the column AND the connection
CREATE TABLE posts (body VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci);
SET NAMES utf8mb4;

-- PostgreSQL: any UTF8 database already stores every emoji
-- SQL Server: use nvarchar, and remember LEN() counts UTF-16 units

为什么没有一劳永逸的正则

正则引擎中的每个表情属性都来自编译进该引擎的 Unicode 数据,所以模式的新旧程度取决于运行它的运行时。我们自己的测试就说明了这一点:\p{RGI_Emoji} 在 3,963 个中只匹配了 3,944 个,因为最新的 19 个序列还不在 Node 25.2.1 的 Unicode 17.0 表中。同样的代码在升级运行时后就能匹配它们,而使用旧浏览器的用户则会继续漏掉。如果某个校验必须在新表情发布当天就接受它们,请按结构(Extended_Pictographic 加上修饰符、连接符和选择符)或按字素匹配,只在漏判无害的地方使用该属性。

同一个表情可能有三种存储形式

Unicode 列出了 3,963 个 fully-qualified 表情,另有同一图案的 1,029 个 minimally-qualified 形式和 243 个 unqualified 形式,大多是不带样式选择符 U+FE0F 的版本。❤ 和 ❤️ 在很多应用中看起来一样,却是不同的字符串,因此唯一性检查、反应计数或搜索索引会把它们当成两样东西。Unicode 规范化也帮不上忙:NFC 和 NFKC 都不会处理变体选择符。如果要比较或去重表情,请确定一种形式(fully-qualified 最稳妥),并在输入时依据 qualified 列表进行转换,而不是删除所有 U+FE0F,那样还会破坏 1️⃣ 这样的键帽。

表情 Unicode 转换器全部 ZWJ 序列及其大小能正确统计表情的字数统计工具表情符号术语表

开发者关于表情的常见问题

为什么在 JavaScript 中一个表情的长度是 2?

JavaScript 字符串使用 UTF-16,任何高于 U+FFFF 的字符都要占用两个 16 位单元,即一个代理对。几乎所有表情都位于 U+FFFF 以上,所以即使是简单的 😀,.length 也是 2。要数码点请用 [...str].length,要数读者看到的数量请用 Intl.Segmenter。

匹配表情的最佳正则是什么?

在现代 JavaScript 中,/\p{RGI_Emoji}/v 可以匹配完整的表情序列,包括国旗和键帽,但仅限于引擎所知道的 Unicode 版本。\p{Extended_Pictographic} 会漏掉国旗和键帽,而 \p{Emoji} 会误匹配数字、# 和 *。

怎样把一个表情算作一个字符?

统计字素簇。在 JavaScript 中使用 granularity 为 grapheme 的 Intl.Segmenter;在 Swift 中,String.count 本身就是这样计数的;在 Python 中使用第三方 regex 模块的 \X;在 PHP 中使用 intl 扩展的 grapheme_strlen。

存储一个表情需要多少字节?

在 UTF-8 中需要 3 到 35 个字节。高于 U+FFFF 的单码点表情占 4 个字节。肤色、国旗和连接序列会累加:在我们的测试中,59% 的表情可以放进 8 个字节,而最长的那个,带两种肤色的亲吻表情,占 35 个字节。

为什么我的表情在数据库里变成了 ?????

列或连接使用的字符集无法存储四字节字符,通常是 MySQL 旧版的 utf8。请把表和连接都切换为 utf8mb4。我们的 Unicode 转换器页面会逐步讲解具体的 ALTER TABLE 语句。