emoji
最后更新 2026-11-02
- emoji
- 表情
- 码位
- 字节
- utf8mb4
- 长度
- 字素簇
- 渲染
- ZWJ
EN中文
本页目录
emoji 最容易让人误判的地方只有一个:它是字符,不是图片。所以它受编码、长度、字符集、字体这几套规则约束——而这几套规则对“一个 emoji”的计数方式各不相同。
它是字符
😀 是一个码位(U+1F600),和其他字符一样存在文本里。你看到的那张彩色脸,是字体把它画出来的。
推论(都很关键):
- 同一段文字,在 Apple、Google、Microsoft 的系统上长得不一样——那是三家各自的 emoji 字体在设计;
- 截图与设计稿里的 emoji 换台机器就不可复现;
- 字体可以完全不带 emoji 字形,那时浏览器会回退到系统的 emoji 字体(见 整形与回退);
- 彩色 emoji 是用位图或矢量表实现的(
CBDT、sbix、COLR等),但对使用它的代码来说,它仍然只是一个字符。
三种长度
同一个字符串,三种算法给出三个数:
| 问法 | 怎么算 | "😀" |
"👨👩👧" |
|---|---|---|---|
| 多少个 UTF-16 码元 | JavaScript 的 .length、HTML 的 maxlength |
2 | 8 |
| 多少个码位 | [...str].length、Array.from(str).length |
1 | 5 |
| 多少个字素簇(看起来的“一个字”) | Intl.Segmenter |
1 | 1 |
家庭 emoji 由“男人 + 零宽连接符 + 女人 + 零宽连接符 + 女孩”拼成:五个码位,其中两个 U+200D 自己不占宽度,但决定了前后要不要拼成一个。
'😀'.length // 2 ← 码元
'😀'.codePointAt(0) // 128512
[...'😀'].length // 1 ← 码位
[...new Intl.Segmenter().segment('👨👩👧')].length // 1 ← 字素簇
要做“字符计数器”(输入框剩余字数、昵称长度限制),先想清楚要数哪一种。多数产品要的是字素簇,而实现里用的往往是 .length。
字节数
UTF-8 下,每个 emoji 通常是 4 字节(BMP 之外的码位都是 4 字节):
| 文本 | 码位 | UTF-16 码元 | UTF-8 字节 |
|---|---|---|---|
A |
1 | 1 | 1 |
中 |
1 | 1 | 3 |
😀 |
1 | 2 | 4 |
👨👩👧 |
5 | 8 | 18 |
new TextEncoder().encode('😀').length // 4
Buffer.byteLength('😀', 'utf8') // 4(Node)
“按字节算”与“按字符算”在这里能差 4 倍——数据库里的很多问题都源于此。
数据库里的长度
先区分两类问题,它们长得像、原因完全不同:
一是字符集放不下。 MySQL 的 utf8 实际上是 utf8mb3(每字符最多 3 字节),存 4 字节的 emoji 会直接失败:
Incorrect string value: '\xF0\x9F\x98\x80' for column 'name' at row 1
这不是长度不够,是列(或连接)的字符集只支持 3 字节。解决:库、表、列、连接全部用 utf8mb4。
二是长度口径不一致。
VARCHAR(n)在 MySQL 里按字符计(n个字符,不是字节);PostgreSQL 同样按字符;SQLite 基本不限;- 但“字符”在 MySQL 里指码位,不是字素簇——一个家庭 emoji 算 5 个;
LENGTH()返回字节,CHAR_LENGTH()返回字符(Postgres 是OCTET_LENGTH()与LENGTH()):
用SELECT LENGTH('😀'), CHAR_LENGTH('😀'); -- 4, 1LENGTH()去做业务判断,中文和 emoji 都会立刻超标;- 索引前缀有字节上限:旧行格式(
COMPACT)是 767 字节,utf8mb4每字符 4 字节,于是VARCHAR(191)成了当年的标准写法;DYNAMIC行格式下是 3072 字节(约VARCHAR(768))。
前端与后端的口径
同一句话,三层给的数可能都不一样:
| 层 | 按什么算 | "😀" |
|---|---|---|
HTML maxlength / JS .length |
UTF-16 码元 | 2 |
数据库 VARCHAR(n) |
码位 | 1 |
校验用的正则 .(没加 u 标志) |
码元 | 2 |
于是“前端说还能输 1 个字,后端说超长”这种情况会出现。通用做法:
- 前端用
[...value].length或Intl.Segmenter计数,别直接用.length; - 后端按字符(
CHAR_LENGTH)校验; - 存之前统一
normalize('NFC')(见 字符属性); - 永远不要按字节截断字符串——那会切出半个代理对,得到乱码。
渲染:它不是一张图片
因为 emoji 是字符,它的样子由字体 + 系统决定:
- Apple Color Emoji、Segoe UI Emoji、Noto Color Emoji 是三家各自的设计,同一个码位画法不同;
- 有的字符有两个“样子”(文本式与 emoji 式),用变体选择符指定:
U+FE0F要 emoji 式,U+FE0E要文本式; - 系统或字体没跟上 Unicode 版本时,新 emoji 会显示成豆腐块。
所以“设计稿上是这个 emoji”在别的机器上可能变成另一个形状——这不是 bug,是字符的正常行为。
在本站能看到什么
- 字符抽屉里能查到任意字符的码位与各语言转义(星平面字符的
\uD83D\uDE00写法尤其容易写错); - 覆盖率里通常看不到 emoji:普通西文/中文字体不含 emoji 字形,页面上那些 emoji 是系统 emoji 字体画的;
- 度量面板测 emoji 时,如果字体没有它的字形,会明确告诉你“字体里没有这个字符”。