码位、U+、平面与代理对

最后更新 2026-11-02

EN中文
本页目录

Unicode 的第一件事是把「字符」变成一个编号,这个编号叫码位(code point)。它和字符在文件里怎么存(字节)是两回事——搞清楚这一层,后面那些「长度为什么是 2」「为什么一个字要 7 个码位」才有解释。

码位就是编号

每个字符有一个 0 到 0x10FFFF 之间的编号。写作 U+ 加至少四位十六进制:

字符 码位
A U+0041
中 U+4E2D
😀 U+1F600

U+ 后面的位数不是固定的:BMP 内的码位习惯写四位,超出时写五位或六位。

平面

把 0–0x10FFFF 每 65,536 个码位切成一块,叫一个平面(plane):

平面 范围 内容
0 U+0000–U+FFFF 基本多文种平面(BMP),绝大多数日常字符
1 U+10000–U+1FFFF 补充多文种平面:emoji、古文字
2 U+20000–U+2FFFF 补充表意平面:CJK 扩展 B 等
3–13 —— 未分配
14 U+E0000–U+EFFFF 补充特殊用途
15–16 U+F0000–U+10FFFF 私用区

「私用区」是留给各家自己定义的:字体可以在这里放任何图形,Unicode 不管它叫什么。

代理区不是字符

U+D800–U+DFFF 这一千多个码位永远不对应字符,它们是 UTF-16 的机制:一个 BMP 之外的字符用两个 16 位码元拼出来,这对码元就落在这个区间里。

所以「代理区里有没有字符」这个问题没有意义——它们只是编码的中转站。

UTF-8 与 UTF-16

  • UTF-8 用 1–4 个字节表示一个码位,ASCII 兼容;网页与文件默认用它;
  • UTF-16 用 2 个字节(一个码元)表示 BMP,BMP 之外用两个码元(代理对)。

JavaScript 的字符串是 UTF-16 码元序列,于是:

'😀'.length                  // 2  ← 两个码元
[...'😀'].length             // 1  ← 一个码位
'😀'.codePointAt(0)          // 128512 (0x1F600)
String.fromCodePoint(0x1F600) // '😀'

同样,slice(0, 1) 会把一个 emoji 砍成半个代理对,得到乱码。要按「字符」遍历,用 for...of 或展开成数组,而不是下标。

一个「字」可能是好几个码位

码位是编码单位,不是「看起来的一个字」:

  • é 可以是 U+00E9 一个码位,也可以是 U+0065 + U+0301(字母 + 重音);
  • 家庭 emoji 👨‍👩‍👧 是「男人 + 零宽连接符 + 女人 + 零宽连接符 + 女孩」,五个码位;
  • 国旗 emoji 是两个「区域指示符」拼出来的。

负责把这些码位组合成「一个视觉字符」的过程叫字素簇(grapheme cluster)。它由 Unicode 的分段规则决定,不是字节数。

变体选择符

有些字符有多个字形(比如「︎」与「️」这类 emoji 的文本/图形两种样子),用变体选择符指定:

  • U+FE0E:要文本样式;
  • U+FE0F:要 emoji 样式。

它们本身不显示,只是「附在前一个字符上」的修饰。字体要在 cmap 的变体序列表里声明支持,否则选择符会被忽略。

本站怎么展示

字符抽屉里显示的是 U+4E2D 这种标准写法,加一行十进制,方便你去别处查。区块用的是 Unicode 官方 Blocks.txt 的名字与范围。

延伸阅读

  • Unicode 标准第 2 章(General Structure)
  • UAX #29(字素簇边界)
  • MDN:String#codePointAt、String.fromCodePoint