零帧起手,先说结论:
Unicode 不是某一种具体的存储格式,它更像一套“统一编号标准”;UTF-8 则是把这套编号真正编码成字节的一种实现方式。
很多人第一次接触这两个概念时,容易把它们混在一起。原因也很简单:它们总是一起出现,而且都和“编码”有关。但如果一开始就没把这层关系理顺,后面看乱码、字符集、文件编码时,就很容易越看越乱。
这篇文章就讲清楚两件事:
Unicode解决了什么问题。UTF-8又是怎么把Unicode落地的。
为什么会乱码?
先举个不严谨但很好懂的例子。
假设郭德纲和于谦各自发明了一套“字符表”。
- 郭德纲规定:
1 -> 郭 - 于谦规定:
1 -> 于
两个人都写了一篇文章,再把文章转换成自己那套编号。问题来了:如果你只拿到一串数字,却不知道它到底遵循的是哪一套规则,那你在还原时就可能把“郭”读成“于”,把“于”读成“郭”。
乱码本质上就是这个问题:同样一串字节,在不同规则下会被解释成不同字符。
我们平时看到的 锟斤拷,很多时候就是“拿错了规则去解码”的结果。
Unicode 解决了什么?
既然问题出在“大家各用各的规则”,那最直接的办法就是:尽量统一规则。
于是就有了 Unicode。
你可以把 Unicode 理解成一个超大的“全球统一字符表”。它试图给世界上常见的文字、符号、表情等都分配一个统一编号。这样一来,不管你在中国、日本、美国还是别的地方,只要大家都认这套编号,至少“这个字符是什么”这件事就能先统一下来。
注意,这里统一的是“字符和编号的对应关系”,不是“这个编号在计算机里具体怎么存”。
这点非常重要。
也就是说,Unicode 更像是在回答:
“这个字符应该对应哪个码点?”
而不是回答:
“这个码点最终要用几个字节保存?”
下面这两张图,可以把它理解成 Unicode 字符集资料的一部分。
你会发现里面有一些字形看起来很像。这是因为 Unicode 里有“中日韩统一表意文字”这一类内容,而同一个字在不同地区可能会有细微写法差异。为了兼容不同语言环境,这些差异通常都会被认真处理,而不是简单粗暴地“只留一个”。
所以,Unicode 做的事,本质上是统一映射关系:哪个字符对应哪个码点。
那 UTF-8 又是什么?
讲到这里,就该轮到 UTF-8 出场了。
如果说 Unicode 负责规定“每个字符的编号”,那 UTF-8 负责的就是:
“怎样把这个编号编码成字节,真正存进文件,或者真正发到网络里。”
也就是说,UTF-8 是 Unicode 的一种编码实现。
除了 UTF-8,还有其他实现方式,比如 UTF-16、UTF-32。更早期的场景里,你也可能见过 UCS-2 这样的名字。
它们做的是同一类事情:把 Unicode 码点变成计算机真正能保存和传输的二进制数据。
为什么同一个字符可以有不同实现?
这其实不难理解。
同一个结果,往往可以有不同实现方式。
比如:
(4 + 6) * 5 = 50
你可以先算括号:
(4 + 6) * 5 = 10 * 5 = 50
也可以直接展开:
(4 + 6) * 5 = 4 * 5 + 6 * 5 = 20 + 30 = 50
最后结果一样,但过程不一样。
Unicode 的不同编码实现,也可以这么理解:目标都是把同一个字符正确表示出来,但具体怎么编码、占多少字节、是否兼容旧系统,这些实现细节可以不同。
UTF-8 是怎么编码的?
对于初学者来说,不一定要把所有二进制规则背下来,但你至少要知道 UTF-8 的核心特点:
- 它是一种可变长度编码。
- 不同字符会占用不同字节数。
- 它会根据字符对应的码点范围,决定使用 1、2、3 或 4 个字节。
你可以粗略把这个过程理解为:
- 先找到这个字符在
Unicode里的码点。 - 再根据码点范围,决定应该用几字节编码。
- 最后按
UTF-8的规则,把码点拆进对应的字节结构里。
所以,UTF-8 不是随便“塞进去”几个字节,而是有固定格式的。
const arr = ["A", "z", "5", "¢", "é", "ß", "你", "好", "中", "😀", "🚀", "𠮷"];
function toUTF8Bytes(str: string) {
const encoder = new TextEncoder();
return encoder.encode(str);
}
arr.forEach(char => {
const bytes = toUTF8Bytes(char);
const binary = Array.from(bytes)
.map(b => b.toString(2).padStart(8, "0"))
.join(" ");
console.log(`${char} -> ${binary}`);
});
其中最关键的,就是每种长度对应的前导位模式。你可以把它理解成一种“字节头信息”,解码器看到这些前导位,就知道这一段数据该按几字节来读。
| 字节数 | 二进制格式 | 说明 |
|---|---|---|
| 1 字节 | 0xxxxxxx | 兼容 ASCII 单字节 |
| 2 字节 | 110xxxxx 10xxxxxx | 双字节编码 |
| 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx | 三字节编码 |
| 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 四字节编码 |
为什么 UTF-8 这么常见?
UTF-8 能流行起来,原因很实际。
第一,它兼容 ASCII。很多早期英文世界的软件和协议,本来就是围绕 ASCII 建起来的。UTF-8 对这部分兼容得很好,迁移成本相对低。
第二,它对常见文本场景很友好。英文内容占用空间小,而中文这类常见字符通常用 3 个字节,也能接受。
第三,它适合网络传输。今天你打开网页、写接口、存配置文件,看到的大多数“文件编码是 UTF-8”,本质上都是因为它在兼容性、空间占用和通用性之间取得了一个很不错的平衡。
最后总结
把这两个概念分开,很多问题就会一下子清楚:
Unicode解决的是“字符应该如何统一编号”。UTF-8解决的是“这个编号在计算机里如何编码存储”。
前者更像统一标准,后者更像具体实现。
所以以后再看到“文件是 UTF-8 编码”,你脑子里最好自动翻译成一句话:
“这份文件里的字符,遵循的是 Unicode 的字符体系;落地存储时,使用的是 UTF-8 这种编码方式。”