极简工具箱

免费在线工具 · 本地计算 · 无需上传

Unicode、UTF-8、UTF-16 到底什么关系

「Unicode 是一种编码」这句话只对了一半。搞不清字符集和编码方案的区别,就会想不明白为什么同一个字在不同系统里字节数不一样、为什么一个 emoji 的长度是 2。

广告位(百度联盟)

一、先分两层:字符集 vs 编码方案

所以「Unicode 编码」这个说法其实不准确:Unicode 是表,UTF-8 才是落地的字节格式。同一句中文,用 UTF-8 存和用 UTF-16 存,字节数完全不同,但表示的内容一致。

二、UTF-8 的变长规则

UTF-8 用 1–4 个字节表示一个码点,靠首字节的高位标记长度:

码点范围字节数首字节形态
U+0000–U+007F10xxxxxxx
U+0080–U+07FF2110xxxxx
U+0800–U+FFFF31110xxxx
U+10000–U+10FFFF411110xxx

好处有两个:完全兼容 ASCII(英文仍是 1 字节,老代码不用改),以及自同步(从任意字节位置都能靠首字节判断边界,坏一个字节不会连累后面全部)。中文常用字落在 3 字节区,所以一个汉字在 UTF-8 里通常是 3 字节,在 GBK 里是 2 字节——这就是「UTF-8 中文更占空间」说法的来源。

三、UTF-16 与代理对

UTF-16 用 2 字节或 4 字节。基本平面(BMP)的字符直接 2 字节,超出 BMP 的(emoji、部分生僻字、音乐符号)要用一对代理项(surrogate pair)拼出 4 字节。这带来一个非常实际的问题:

'👍'.length        // 2  ← UTF-16 码元数,不是字符数
[...'👍'].length    // 1  ← 按码点迭代才是 1
'👨‍👩‍👧'.length      // 8  ← 带零宽连接符的复合 emoji

JavaScript 的字符串长度按 UTF-16 码元计算,所以做「输入不超过 10 个字」这类限制时,用 .length 会把一个 emoji 算成 2,甚至把家庭 emoji 算成 8。正确做法是用 Array.from(str).length 或 Intl.Segmenter 按字素簇切分。

四、\u 转义与 \u{...}

JSON 和多数语言支持 \uXXXX 表示一个 UTF-16 码元。注意它不能直接表示 4 字节字符,必须写成代理对:

{"emoji": "\ud83d\udc4d"}

JavaScript 的 ES6 之后可以用 \u{1F44D} 写完整码点,但 JSON 标准不支持这种写法。所以接口里出现 \ud83d\udc4d 这种成对转义是正常的,不要以为是数据损坏。

五、乱码的根源只有一句话

写入时用的编码,和读取时声明的编码不一致。 文件、HTTP 响应头、HTML 的 <meta charset>、数据库连接串、编辑器的保存编码,任何一处不统一就会乱码。排查顺序是:先确认原始字节到底是什么编码(看字节特征,别猜),再让读取方按同一编码解析。

遇到 %E4%B8%AD 这类串,可以用 URL 编码解码 还原;想查某个字符的码点和 UTF-8 字节,用 Unicode 转换工具 直接看,不必背表。

小结

Unicode 是编号表,UTF-8/UTF-16 是存法;UTF-8 变长且兼容 ASCII,中文 3 字节;JS 的 .length 数的是 UTF-16 码元,emoji 会算成 2;乱码只有一个原因——编解码不一致。

本文相关工具(本地计算,无需上传)

更多使用教程