> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/10-zi-fu-bian-ma-na-dian-shi-asciiunicodegbkutf8.md).

# 字符编码那点事：ASCII、Unicode、GBK、UTF 8

数字计算机中的存储器唯一可以存储的是比特（bit），因此如果要想在计算机上处理信息，就必须把它们按位存储。为了将文本表示为数字形式，我们需要构建一种系统来为每一个字母赋予一个唯一的编码。数字和标点符号也算做文本的一种形式，所以它们也必须拥有自己的编码。

所有由符号所表示的字母和数字（Alphanumeric）都需要编码。具有这种功能的系统被称为字符编码集（Coded Character Set），系统内的每个独立编码称为字符编码（Character Codes）。

所有人都遵循并使用统一化的字符编码，不同计算机之间就可以互相交流文本信息。

* **字节（Byte）：** 是计量存储容量的单位，是构成信息的一个很小的单位，上面还有 KB、MB、GB、TB、PB、EB、ZB、YB 等；下面还有 bit（位）。
* **字符：** 各种文字、符号的总称。例如文字、标点、图形、数字、字母等等。
* **字符集：** 顾名思义，就是一定数量的字符组成的集合，字符集种类比较多，而且每个字符集包含的字符个数也不同，常见字符集主要有：ASCII 字符集、GB2312 字符集、BIG5 字符集、 GB18030 字符集、Unicode 字符集等，字符集为每一个【字符】分配一个唯一的 ID（学名为码位 / 码点 / Code Point）。
* **字符编码：** 将字符集中的每个字符映射为字节流的实现方案（编码方案），即属于将【码位】转换为字节序列的规则，便于计算机存储和传输；常见的字符编码有 ASCII 编码、UTF-8 编码、GBK 编码、Base64 编码等。某种意义上来说，字符集与字符编码有种对应关系，例如 ASCII 字符集对应有 ASCII 编码。
* **编码与解码：** 编码的过程是将字符转换成字节流，解码的过程是将字节流解析为字符。

## ASCII

ASCII (American Standard Code for Information Interchange，美国信息交换标准代码）是基于拉丁字母的一套电脑编码系统，主要用于显示现代英语和其他西欧语言，共定义了128个字符 。从1967年正式公布至今，它一直是计算机产业中最重要的标准。

ASCII 码一共规定了128个字符的编码，比如空格SPACE是32（二进制00100000），大写的字母A是65（二进制01000001）。这128个符号（包括32个不能打印出来的控制符号），只占用了一个字节的后面7位，最前面的一位统一规定为0。

ASCII 字符集沿用至今，但它最大的缺点在于只适用于以英语为主的国家，在面对数以万计的中国、日本、韩国的象形文字，以及奇怪的朝鲜文音节等各国语言面前就不够用了。

为了应对各种语言的编码需求，近几十年来出现了许多不同版本的扩展的 ASCII 码，多个不同的版本严重影响了编码的一致性，导致了混淆和不兼容。其中通用的 ASCII 码字符，是用单个字节编码表示的，相比而言，成千上万的象形文字则是双字节编码，这在无形之中增加了使用这种字符集的难度。

## Unicode

Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。

Unicode，统一码，也叫万国码，学名是"Universal Multiple-Octet Coded Character Set"，简称为 UCS。现在用的是 UCS-2，即2个字节编码，而 UCS-4 是为了防止将来2个字节不够用才开发的。全世界所有的人类语言，尤其是经常出现在计算机通信过程中的语言，都可以使用同一个编码系统，而且这种系统还具备很高的扩展性。

Unicode 至今仍在不断增修，每个新版本都加入更多新的字符。目前最新的版本为2021年9月公布的 14.0.0，已经收录超过 14 万个字符（第十万个字符在2005年获采纳）。Unicode 除了视觉上的字形、编码方法、标准的字符编码资料外，还包含了字符特性（如大小写字母）、书写方向、拆分标准等特性的资料库。

**需要注意的是，** **Unicode 只是一个符号集，它只规定了符号的二进制代码，却没有规定这个二进制代码应该如何存储。**

比如，汉字严的 Unicode 是十六进制数 4E25，转换成二进制数足足有15 位（100111000100101），也就是说，这个符号的表示至少需要2个字节。表示其他更大的符号，可能需要3个字节或者4个字节，甚至更多。

这里就有两个严重的问题，第一个问题是，如何才能区别 Unicode 和 ASCII ？计算机怎么知道三个字节表示一个符号，而不是分别表示三个符号呢？第二个问题是，我们已经知道，英文字母只用一个字节表示就够了，如果 Unicode 统一规定，每个符号用三个或四个字节表示，那么每个英文字母前都必然有二到三个字节是0，这对于存储来说是极大的浪费，文本文件的大小会因此大出二三倍，这是无法接受的。

它们造成的结果是：

* 出现了 Unicode 的多种存储方式，也就是说有许多种不同的二进制格式，可以用来表示 Unicode。
* Unicode 在很长一段时间内无法推广，直到互联网的出现。

## UTF-8

虽然 Unicode 字符集中的码位唯一，但由于计算机存储数据通常是以字节为单位的，而且出于兼容之前的 ASCII、大数小段数段、节省存储空间等诸多原因，通常情况下，我们需要一种具体的编码方式来对字符码位进行存储。

比较常见的基于 Unicode 字符集的编码方式有 UTF-8、UTF-16 及 UTF-32。

UTF-8（8位元，Universal Character Set/Unicode Transformation Format）是针对 Unicode 的一种可变长度字符编码。它可以用来表示 Unicode 标准中的任何字符，而且其编码中的第一个字节仍与ASCII相容，使得原来处理ASCII字符的软件无须或只进行少部分修改后，便可继续使用。

UTF-8 使用 1\~4 字节为每个字符编码：

* 128个 US-ASCII 字符只需一个字节编码（Unicode 范围由 U+0000至 U+007F）。对于只有一个字节的符号，字节的第一位设为0，后面 7 位为这个符号的 Unicode 码。
* 对于 n 字节的符号，第一个字节的前 n 位都设为 1，第 n + 1 位设为0，后面字节的前两位一律设为 10。剩下的没有提及的二进制位，全部为这个符号的 Unicode 码。
  * 2字节编码：带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文等字母（Unicode范围由U+0080\~U+07FF）。
  * 3字节编码：其他语言的字符（包括中日韩文字、东南亚文字、中东文字等）包含了大部分常用字。
  * 4字节编码：其他极少使用的语言字符。

**UTF-8 因可以兼容 ASCII，且较为节约存储空间，而被广泛使用。**

UTF-16 使用 2 或 4 个字节为每个字符编码，但 UTF-16 却无法兼容于 ASCII 编码。

UTF-32 使用四个字节为每个字符编码，使得 UTF-32 占用空间通常会是其它编码的二到四倍。

现行桌面系统中，Windows 内部采用了 UTF-16 的编码方式，而 Mac OS、Linux 等则采用了 UTF-8 编码方式。

## GB2312

除了基于 Unicode 字符集的 UTF-8、UTF-16 等编码外，在中文语言地区，我们还有一些常见的字符集及其编码方式，GB2312、Big5 就是其中影响最大、使用最广泛的两种。

**GB2312 或 GB2312-80 是中国国家标准简体中文字符集**，全称《信息交换用汉字编码字符集·基本集》，又称 GB 0，由中国国家标准总局发布，1981 年 5 月 1 日实施。GB2312 编码通行于中国大陆；新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持 GB 2312。

GB2312 的出现先于 Unicode。**GB2312 标准共收录 6763 个汉字**，其中一级汉字 3755 个，二级汉字 3008 个；同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符，而在编码上，是采用了基于区位码的一种编码方式，采用 2 字节表示一个中文字符。

* GB 2312 的出现，基本满足了汉字的计算机处理需要，它所收录的汉字已经覆盖中国大陆 99.75% 的使用频率。
* 对于人名、古汉语等方面出现的罕用字，GB 2312 不能处理，这导致了后来 GBK 及 GB 18030 汉字字符集的出现。

不同的编码方式对于相同的二进制字符串的解释是不同的。常见的，如果一个 UTF-8 编码的网页中的字符串按照 GB2312 编码进行显示，就会出现乱码。

## Big5

BIG5 则常见于繁体中文，俗称“大五码”。BIG5 是长期以来的繁体中文的业界标准，共收录了 13060 个中文字，也采用了 2 字节的方式来表示繁体中文。BIG5 在中国台湾、香港、澳门等地区有着广泛的使用。

而 BIG5 和 GB2312 之间的乱码则在中文地区软件中有着“悠久”的历史。不过随着 Unicode 的使用和发展，以及软件系统对多种编码的支持，程序发生乱码的现象也越来越少。

## GBK

**GBK** 即**汉字内码扩展规范**，**K** 为汉语拼音 Kuo Zhan（扩展）中“扩”字的声母。英文全称 Chinese Internal Code Specification。

**GBK 共收入 21886 个汉字和图形符号**，包括：

* GB 2312 中的全部汉字、非汉字符号。
* BIG5 中的全部汉字。
* 与 ISO 10646 相应的国家标准 GB 13000 中的其它 CJK 汉字，以上合计 20902 个汉字。
* 其它汉字、部首、符号，共计 984 个。

GBK 向下与 GB 2312 完全兼容，向上支持 ISO 10646 国际标准，在前者向后者过渡过程中起到的承上启下的作用。

## GB18030

GB18030，全称：国家标准 GB 18030-2005《信息技术中文编码字符集》，是中华人民共和国现时最新的内码字集，是 GB 18030-2000《信息技术信息交换用汉字编码字符集基本集的扩充》的修订版。GB 18030 与 GB 2312-1980 和 GBK 兼容，共收录汉字70244个。

* 与 UTF-8 相同，采用多字节编码，每个字可以由 1 个、2 个或 4 个字节组成。
* 编码空间庞大，最多可定义 161 万个字符。
* 支持中国国内少数民族的文字，不需要动用造字区。
* 汉字收录范围包含繁体汉字以及日韩汉字

**GB 18030 编码是一二四字节变长编码。**

* 单字节，其值从 0 到 0x7F，与 ASCII 编码兼容。
* 双字节，第一个字节的值从 0x81 到 0xFE，第二个字节的值从 0x40 到 0xFE（不包括0x7F），与 GBK 标准兼容。
* 四字节，第一个字节的值从 0x81 到 0xFE，第二个字节的值从 0x30 到 0x39，第三个字节从0x81 到 0xFE，第四个字节从 0x30 到 0x39。

## 编码解码方法

我们在操作 URL 的时候经常需要处理其中的特殊字符，因为在 URL 中有很多字符是有特殊意义的，比如 `?、&、=` 等等，如果在处理 URL 的时候不对这些特殊字符进行转义，那么浏览器很可能会错误解析 URL，所以一般的编程语言都提供了对 URL 进行编码解码的 API。

### URI 和 URL 的区别

URI 是 uniform resource identifier，统一资源**标识符**，用来唯一的标识一个资源。

URL 是 uniform resource locator，统一资源**定位器**，它是一种具体的 URI，即 URL 可以用来标识一个资源，而且还指明了如何定位这个资源。

笼统地说，每个 URL 都是 URI，但不一定每个 URI 都是 URL。

URI 一般由三部分组成：

* 访问资源的命名机制；
* 存放资源的主机名；
* 资源自身的名称，由路径表示。

```shell
https://www.example.com/path/source#hash
file://user/lizh/demo-lizh/test.txt
www.example.com/path/source#hash
```

URL 的格式一般由下列三部分组成：

* 第一部分是协议（或称为服务方式）；
* 第二部分是存有该资源的主机IP地址（有时也包括端口号）；
* 第三部分是主机资源的具体地址。

```shell
https://www.example.com/path/source#hash
file://user/lizh/demo-lizh/test.txt
# 注意：www.example.com/path/source#hash 不是有效的URL
```

你可能觉得 URI 和 URL 可能是相同的概念，其实并不是，URI 和 URL 都定义了资源是什么，但 URL 还定义了该如何访问资源。URL 是一种具体的 URI，它是 URI 的一个子集，它不仅唯一标识资源，而且还提供了定位该资源的信息。URI 是一种语义上的抽象概念，可以是绝对的，也可以是相对的，而 URL 则必须提供足够的信息来定位，是绝对的。

### URL 编码

URL 编码（URL encoding），也称百分号编码，是特定上下文的统一资源定位符 （URL）的编码机制，实际上也适用于统一资源标志符（URI）的编码。URL 的编码格式采用的是 `ASCII` 码，而不是 `Unicode`，这也就是说你不能在 `URL` 中包含任何非 `ASCII` 字符。否则，如果客户端浏览器和服务端浏览器支持的字符集不同的情况下，可能会造成问题。

RFC3986 文档规定，URL 中只允许包含未保留字符以及所有保留字符，**其它字符必须用 URL（百分号）编码**。

* 保留字符是那些具有特殊含义的字符（18个）：`: / ? # [ ] @ ! $ & ' ( ) * + , ; =` ；
* 未保留字符没有这些特殊含义的字符（66个）：`a-z A-Z 0-9 - _ . ~` 。
* 受限字符或不安全字符，直接放在 Url 中的时候，可能会引起解析程序的歧义。比如：空格、`% < > " { }` | `\ ^ ~ [ ] '`、`0x00-0x1F`、 `0x7F`、`>0x7F`。

**注意：保留字符和未保留字符也有可能是受限或不安全字符。**

比如： URL 参数字符串中使用 `key=value` 键值对这样的形式来传参，键值对之间以 `&` 符号分隔，如 `/index?v=1.0.0&author=lizhao`。如果你的 `value` 字符串中包含了 `=` 或者 `&`，那么势必会造成接收 URL 的服务器解析错误。

### escape()/unescape()

escape() 方法通过用 **十六进制转义序列** 替换某些字符来对 URI 进行编码。

escape 编码规则：

* 不对 ASCII 字母、数字进行编码，即 `a-z A-Z 0-9`。（62个）
* 不对 `* @ - _ + . /` 进行编码。（7个）
* 其他所有的字符都会被转义序列替换为 %xx 或 %uxxxx（x表示十六进制的数字）的转义序列。从 \u000 到 \u00ff 的 Unicode 字符由转义序列 %xx 替代，其他所有 Unicode 字符由 %uxxxx 序列替代。

escape的不编码集（69个）：

```shell
A-Z a-z 0-9 * @ - _ + . /
```

> **注意：** `escape()/unescape()` 已经从 Web 标准中删除，虽然一些浏览器目前仍然支持它，但也许会在未来的某个时间停止支持，请尽量不要使用该特性。
>
> 使用 encodeURI 或 encodeURIComponent 代替。

```javascript
escape("abc123");     // "abc123"
escape("äöü");        // "%E4%F6%FC"
escape("ć");          // "%u0107"

escape("@*_+-./");    // "@*_+-./"
```

unescape() 方法是对 escape() 方法编码的字符串进行解码。

### encodeURI()/decodeURI()

encodeURI() 方法通过用 **1\~4个字节表示字符的 UTF-8 编码的转义序列** 替换某些字符来对 URI 进行编码。

以下示例显示了 URI 可能包含的所有部分，请注意某些字符表示特殊含义：

```shell
http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this+has+spaces#anchor
# 包含的保留字符9个：/?:@&=+.#
```

encodeURI 的编码规则：

* 不对 ASCII 字母、数字进行编码，即 `a-z A-Z 0-9`。（62个）
* 不对 `; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #` 进行编码。（20个）
* 其他所有的字符都会被转义序列替换。

encodeURI 的不编码集（82个），即 82 = 保留字符（18个）+ 未保留字符（66个） - 不安全的保留字符`[]`（2个）：

```shell
A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
```

decodeURI() 方法对 encodeURI() 方法编码的字符串进行解码。

**注意 escape() 和 encodeURI() 的区别：**

* 两者的转换结果不一样：escape() 方法转换为 **十六进制转义序列** ，而 encodeURI() 方法转换为 **UTF-8 编码的转义序列**。

  ```javascript
  escape("兆")    // %u5146
  encodeURI("兆") // %E5%85%86
  ```
* escape 的不编码集（69个） 是 encodeURI 的不编码集（82个） 的子集。即 encodeURI() 方法能编码的，escape() 方法一定能编码，反之则不然。

### encodeURICompoent()/decodeURICompoent()

encodeURIComponent() 方法通过用 **1\~4个字节表示字符的 UTF-8 编码的转义序列** 替换某些字符来对 URI 进行编码。

encodeURIComponent 的编码规则：

* 不对 ASCII 字母、数字进行编码，即 `a-z A-Z 0-9`。（62个）
* 不对 `- _ . ! ~ * ' ( )` 进行编码。（9个）
* 其他所有的字符都会被转义序列替换。

encodeURIComponent 的不编码集（71个）：

```shell
A-Z a-z 0-9 - _ . ! ~ * ' ( )
```

encodeURIComponent () 方法对 decodeURICompoent() 方法编码的字符串进行解码。

**注意 encodeURI() 和 encodeURIComponent () 的区别：**

它们的主要区别在于，encodeURICompoent() 方法假定参数是 URI 的一部分，例如，协议、主机名、路径或查询字符串。因此，它将转义用于分隔 URI 各个部分的标点符号。而 encodeURI() 方法仅把它们视为普通的 ASCII 字符，并没有转换。

换句话说，encodeURICompoent() 方法会转换 URI 中的分隔符号，而 encodeURI() 方法不会。

再简单点说，encodeURICompoent() 方法会多转换以下符号（11个）：

```shell
; , / ? : @ & = + $ #
```

```javascript
encodeURI(";,/?:@&=+$#")          // ;,/?:@&=+$#
encodeURIComponent(";,/?:@&=+$#") // %3B%2C%2F%3F%3A%40%26%3D%2B%24%23
```

请注意，encodeURI() 它本身不能形成正确的 HTTP GET 和 POST 请求，例如：XMLHttpRequest，因为 `& + =` 没有被编码，它们在 GET 和 POST 请求中被视为特殊字符 。而 encodeURIComponent() 可以对这些字符进行了编码。

为了避免服务器收到不可预知的请求，对任何用户输入的作为 URI 部分的内容你都需要用 encodeURIComponent 进行转义。比如，一个用户可能会输入 "Thyme \&time=again" 作为 URI 变量 key 的一部分。如果不使用 encodeURIComponent 对此内容进行转义，服务器得到的将是 key=Thyme%20\&time=again。请注意，"&" 符号和 "=" 符号产生了一个新的键值对，所以服务器得到两个键值对（一个键值对是 key=Thyme，另一个则是time=again），而不是一个键值对。

## Unicode转换

JavaScript 中，unicode 是以十六进制代码外加开头 `\u` 表示的字符串，即 \uxxxx。

```javascript
// 转unicode
"兆".charCodeAt(0) // 20806
"兆".charCodeAt(0).toString(16) // 5146,对应unicode \u5146

console.log('\u5146') // 兆
'\u5146' === '兆' // true
String.fromCharCode(20806) // 兆
```

```javascript
function encodeUnicode(str) {
    let uString = ''
    for (let i = 0; i < str.length; i ++) {
        let temp = str.charCodeAt(i).toString(16).toUpperCase()
        while (temp.length < 4) {
            temp = '0' + temp
        }
        temp = '\\u' + temp
        uString += temp
    }
    return uString
}
```

## 参考资料

[URI和URL的区别](https://www.cnblogs.com/anke-z/p/13084107.html)

[URL编码的奥秘](https://jelly.jd.com/article/6006b1045b6c6a01506c87e8)

[MDN-encodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI)

[MDN-encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)
