Unicode 与 Emoji 编码完全指南:从码点到渲染,搞懂字符编码的一切

加密编码编码、哈希与二维码

Unicode:统一所有字符的编码标准

Unicode 为世界上几乎所有文字系统的每个字符分配唯一编号(码点)。

概念 说明 示例
码点 (Code Point) 字符的唯一编号 U+4E2D (中)
码平面 每 65536 个码点为一个平面 BMP (0号平面)
BMP 基本多文种平面,U+0000~U+FFFF 大部分常用字符
补充平面 U+10000~U+10FFFF Emoji、古文字、生僻字

一、UTF 编码族

编码 字节长度 "中" 的编码 特点
UTF-8 1-4 字节 E4 B8 AD 变长,兼容 ASCII,Web 首选
UTF-16 2 或 4 字节 4E2D 变长,JS 内部使用
UTF-32 固定 4 字节 00004E2D 定长,内存占用大

UTF-8 编码规则

码点范围 字节数 编码模板
U+0000 ~ U+007F 1 0xxxxxxx
U+0080 ~ U+07FF 2 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

编码示例

字符: 中
码点: U+4E2D = 0100 1110 0010 1101

UTF-8 (3字节):
  1110xxxx 10xxxxxx 10xxxxxx
  1110 0100 10 111000 10 101101
  = E4 B8 AD

UTF-16 (2字节):
  4E2D

UTF-32 (4字节):
  00004E2D

二、JavaScript 的 Unicode 陷阱

字符串长度不准

'中'.length              // 1 ✅
'𠮷'.length             // 2 ❌ (补充平面字符,代理对)
'👨‍👩‍👧‍👦'.length           // 11 ❌ (Emoji ZWJ 序列)
'🏳️‍🌈'.length             // 6 ❌ (旗帜 Emoji)

原因:JS 使用 UTF-16

补充平面的字符(码点 > U+FFFF)需要代理对(2 个 UTF-16 码元)表示:

字符: 𠮷 (U+20BB7)
UTF-16 代理对: D842 DF87
JS: length = 2 (两个码元)

正确处理方式

// 方法一:展开运算符
[...'𠮷'].length           // 1 ✅
[...'👨‍👩‍👧‍👦'].length         // 1 ✅ (但 ZWJ 序列视为一个)

// 方法二:Array.from
Array.from('𠮷').length    // 1 ✅

// 方法三:for...of
for (const char of '𠮷👨‍👩‍👧‍👦') {
  console.log(char);  // 逐字符迭代
}

码点操作

// 获取码点
'中'.codePointAt(0)        // 20013 (0x4E2D)
'𠮷'.codePointAt(0)        // 134047 (0x20BB7)

// 从码点创建字符
String.fromCodePoint(0x4E2D)  // '中'
String.fromCodePoint(0x20BB7) // '𠮷'

// 注意:charCodeAt 无法处理补充平面
'𠮷'.charCodeAt(0)         // 55362 (仅第一个代理)

三、Emoji 编码深度解析

Emoji 的四种类型

类型 示例 码点 长度
基础 Emoji 😀 U+1F600 1 (码点)
修饰符 Emoji 👩🏽 U+1F469 + U+1F3FD 2 (码点)
ZWJ 序列 👨‍👩‍👧‍👦 多码点 + ZWJ 7 (码点)
旗帜 Emoji 🇨🇳 U+1F1E8 + U+1F1F3 2 (码点)

零宽连接符 (ZWJ)

ZWJ (U+200D) 将多个 Emoji 连接为一个复合 Emoji:

👨 (U+1F468) + ZWJ (U+200D) + 👩 (U+1F469) + ZWJ (U+200D) + 👧 (U+1F467) + ZWJ (U+200D) + 👦 (U+1F466)
= 👨‍👩‍👧‍👦 (一家四口)

肤修饰符

肤色修饰符 (U+1F3FB ~ U+1F3FF) 改变 Emoji 的肤色:

👩 (U+1F469) + 🏽 (U+1F3FD) = 👩🏽 (中等肤色女性)
修饰符 码点 肤色
U+1F3FB 🏻 浅肤色
U+1F3FC 🏼 中浅肤色
U+1F3FD 🏽 中等肤色
U+1F3FE 🏾 中深肤色
U+1F3FF 🏿 深肤色

旗帜 Emoji

旗帜由两个区域指示符号组成(ISO 3166-1 alpha-2):

🇨 (U+1F1E8) + 🇳 (U+1F1F3) = 🇨🇳 (中国)
🇺 (U+1F1FA) + 🇸 (U+1F1F8) = 🇺🇸 (美国)
🇯 (U+1F1EF) + 🇵 (U+1F1F5) = 🇯🇵 (日本)

四、变体选择符

VS16 (U+FE0F) 将文本呈现字符转为 Emoji 呈现:

❤ (U+2764)           → 文本呈现:❤ (黑色心形)
❤️ (U+2764 + U+FE0F) → Emoji 呈现:❤️ (红色心形)
字符 无 VS16 有 VS16
☕ ☕ (文本) ☕️ (Emoji)
⚡ ⚡ (文本) ⚡️ (Emoji)
❤ ❤ (文本) ❤️ (Emoji)

五、使用工具库处理 Unicode

Unicode 编解码

  1. 打开 Unicode 编解码工具
  2. 输入文本或码点
  3. 查看转换结果:
    • 文本 → Unicode 转义序列 (\u4E2D)
    • 码点 → 字符
    • UTF-8/16/32 字节序列

Emoji 编码

  1. 打开 Emoji 编码工具
  2. 输入 Emoji
  3. 查看完整编码信息:
    • 码点序列
    • UTF-8 字节
    • ZWJ/修饰符分解
    • CLDR 短名称

六、常见陷阱与解决

陷阱一:字符串截断

// 错误:截断可能切断代理对
text.substring(0, 10)  // 可能产生无效字符

// 正确:按码点截断
function sliceByCodePoint(str, start, end) {
  const chars = [...str];
  return chars.slice(start, end).join('');
}

陷阱二:字符串反转

// 错误:反转破坏组合字符
'👨‍👩‍👧‍👦'.split('').reverse().join('')  // 乱码

// 正确:按码点反转
[...'👨‍👩‍👧‍👦'].reverse().join('')  // ✅

陷阱三:正则匹配

// 错误:. 不匹配补充平面字符
/^.$/.test('𠮷')  // false

// 正确:使用 u 标志
/^.$/u.test('𠮷')  // true

陷阱四:排序

// 默认排序按 UTF-16 码元
['中', '𠮷', 'a'].sort()  // 可能不按预期

// 正确:按码点排序
['中', '𠮷', 'a'].sort((a, b) =>
  a.codePointAt(0) - b.codePointAt(0)
)

七、实用 API

// 规范化(处理组合字符等价)
'café'.normalize('NFC')   // café (组合)
'café'.normalize('NFD')   // café (分解)

// 检测 Emoji
function isEmoji(char) {
  const code = char.codePointAt(0);
  return (code >= 0x1F600 && code <= 0x1F64F) ||  // Emoticons
         (code >= 0x1F300 && code <= 0x1F5FF) ||  // Misc Symbols
         (code >= 0x1F680 && code <= 0x1F6FF) ||  // Transport
         (code >= 0x1F900 && code <= 0x1F9FF);    // Supplemental
}

// 获取字符的真实长度
function graphemeLength(str) {
  const segmenter = new Intl.Segmenter('zh', { granularity: 'grapheme' });
  return [...segmenter.segment(str)].length;
}

graphemeLength('👨‍👩‍👧‍👦')  // 1 ✅
graphemeLength('🏳️‍🌈')    // 1 ✅
#Unicode#Emoji#UTF-8#字符编码#码点