为什么需要了解字符编码
字符概念存在于开发任意环节中:
- 代码文本的开发编辑
- 前端页面的字体显示
- 数据传输的格式处理
你可能会遇到这些
- 举个开发中可能遇到的字符编码相关的概念,
content-type
// Content-Type 实体头部用于指示资源的 MIME 类型 media type。在响应中,Content-Type 标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行 MIME 查找,并不一定遵循此标题的值;
Content-Type: text/html; charset=utf-8
- 服务端在数据库存储也需要考虑编码问题 e.g. 比如在商店,就需要考虑字段存储使用 utf8 还是 utf8mb4
回头过来阅读
接下来
- 计算机字符标准的发展简史
- 稍显细致的角度,了解一下字符的主要概念:字符,字符集,字符编码,码元,码点,BOM
- 稍微在文章中掺杂些开发的实际例子,多联想一下以前有没有写过 bug
- 最后,提供了几篇字符领域不同方向拓展阅读
💾 历史
了解字符编码常见概念,以及字符编码演进过程中的问题
早期:ascii
ASCII, American Standard Code for Information Interchange,美国信息交换标准代码
规定了 128 / 2^7 个字符(字符集)及对应的二进制转换关系(字符编码):
-
ASCII 字符集是字母、数字、标点符号以及控制符(回车、换行、退格)等组成的 128 个字符
-
ASCII 字符编码是将这 128 个字符转换为计算机可识别的二进制数据的一套规则(算法)
[Wikipedia] 的定义
字符编码(英语:Character encoding)是把字符集中的 字符编码 为指定集合中某一 对象,以便 文本 在 计算机 中存储和通过 通信网络 的传递。
计算机通过这个概念可以将存储的二进制字符数据映射到对应的显示对象

早期字符标准时代存在局限
- 字符集只包含英文,标准对于非英语字符兼容性不好 ¯_(ツ)_/¯
后续推出 EASCII 标准,利用了之前未使用的校验 bit,减轻了该问题
- 标准混乱,各个语言有一套自己的标准
一个字符字节在不同标准映射到不同的含义。
"太好了,我们可以去使用 128-255 区间段去放我们自己的语言字符!" 好想法,只是很多人都同时想到了。
一个解决方案是制定国际标准,比如 ISO/IEC 646。但是问题并没有得到缓解,因为除了拉丁语系,东亚的字符需求被忽视了
GB 标准
中国自己制定了一套使用 2 字节来显示字符的标准,以弥补只使用 1 字节来处理字符逻辑的 ASCII/EASCII 缺陷

结构
GB 存在分区的概念,这在 2 字节的结构中也有体现:
暂时无法在飞书文档外展示此内容
分区
每区含有 94 个汉字/符号,共计 94 个区。
每个分区都存储不同类型的字符:
- 01~09 区(682 个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的 682 个全角字符;
- 10~15 区:空区,留待扩展;在附录 3,第 10 区推荐作为 GB 1988—80 中的 94 个图形字符区域(即第 3 区字符之 半形 版本)。
- 16~55 区(3755 个):常用汉字(也称一级汉字),按拼音排序;
- 56~87 区(3008 个):非常用汉字(也称二级汉字),按部首/笔画排序;
- 88~94 区:空区,留待扩展。
这个概念类比到 xx 路 xx 门牌号。
Unicode 标准

Unicode 体系比之前要丰满不少。主要在于字符集虽然只有一个,但是对应的字符编码算法却有数种,且仍然在不断演变之中。
其中为人所知晓的就包含 utf-8,utf-16
Unicode 字符集
console.log("\u{1F60D}");
码点 Code Point
Unicode 字符集将每个字符引申为一个 码点(code point)的概念。这个概念帮助规定了每个字符对应到唯一的代码值(code point),代码值 从 0000 ~ 10FFFF 共 1114112 个值。
字符集 Code Space
基于这个概念,字符集可以重新定义为码点的集合空间,Code Space
这个空间分为 17 个平面,包括 1 个基本平面 BMP (Basic Multilingual Plane) 和 16 个增补平面 SP 在内的共 17 个平面(很类似 GB 标准的分区概念),具体每个平面包含的字符可以查看 Wikipedia;

字符编码方式
码元 Code Unit
这里提出了 码元(code unit)的概念,码元是字符编码系统(例如 UTF-8 或 UTF-16)使用的基本组成部分。可以理解为编码方式处理字符数据的基本单元。
e.g. 比如 utf-16 是以 2 个字节作为基本单位的(即使是在有代理对的情况下),那码元大小是 2 字节。
UCS-2:JavaScript 的早期选择
固定 2 字节码元来表示一个码点。
缺点在于 2 字节无法表明 Unicode 字符集的所有字符,这个问题被 utf-16 以代理对的形式解决。
UTF-16:代理对机制
代理机制就是用两个对应于基本平面 BMP 代理区 (Surrogate Zone) 中的码点编号的 16 位码元,来表示一个增补平面码点,结合此情景,数据格式设计考虑两点:
- 字符平面包含的大小
- 防止码元被单独处理成一个 BMP 字符

除去固定前缀,有效位是 20 bit,刚好能够表示目前 16 个增补平面中的全部码点
pppp xxxx xxxx xxxx xxxx
另外,这两个固定前缀刚好位于 BMP 的未定义字符的区间,所以避免了被识别为有效 BMP 字符的情况。
因为代理对机制的存在,使用 2 字节码元处理实现的 Javascript 会出现这些非预期的事情:
console.log('😂'.length); // 2
console.log('𝒳'[0]) // �
BOM
开头会放置一个 BOM 字节,用来标注字节流的相关信息。其涵义有三:
UTF-8:当前的主流
与 utf-16 一样,是变长字符编码算法,码元大小是一个字节。
通过设几个问题来认识 utf-8 的主要特性:
- 它是一个变长编码算法(最短 1 字节,最长可能 4 字节),所以怎么判断当前码元是一个码点的哪个部分?
- utf-8 有什么优势,使其能成为主流的字符编码?
前缀码
从首字节判断一个 UTF-8 编码有几个字节:
- 0 bit 开头,单字节编码
- 110 bit 开头,双字节
- 1110 bit 开头,三字节

额外再谈谈 utf-8 的好处
- utf-8 在单字节码元情况下完全是 ascii 编码,兼容性也很好
- utf-8 是单字节码元,所以 BOM 对其意义不大
- utf-8 可扩展性好
- 变长码元在一些场景(字节流中间字节丢失/增加/改动/删除)纠错性能好
重新梳理一下
🤓 拓展阅读
-
- 流传较广的字符入门文章,在《JavaScript 高级程序设计》中有提及过
-
- 阿里云的文章,介绍的较为详细,字符机制发展过程中关键的点都有涉及。同时对于 python 和 java 中常见的字符错误额外花篇章进行了描述
-
Javascript 使用 UCS-2 还是 UTF-16 呢?
- 对于 JavaScript 开发者需要注意的,了解一下 JavaScript 在字符编码这块的实现
-
- 阮一峰老师的博文,说是详解,实际可以当作很好的入门读物
- 较为好笑的一点是,阮一峰老师在评论里提及到因为字符的问题导致这篇唠字符的博文在渲染时候出错
-
- 中文社区中非常详细的字符编码系列,对主要概念进行了详尽的描述