0x1461A0

Unicode及其编码方式

Unicode 是一种字符编码标准(字符集),用于表示世界上几乎所有的书写系统的字符。它旨在统一字符表示,使得不同计算机系统和应用程序之间能够正确地交换、处理和显示文本数据。

字符集和字符编码

计算机中储存的信息都是用二进制数表示的,而在屏幕上我们直接看到的是一个个的字符

为了能够将人类使用的字符存储在计算机中:

  1. 给需要表示的每个字符分配一个编号(整数)
  2. 当需要使用某个字符时直接用这个编号就可以了

一个系统支持的所有抽象字符的集合(字符和其对应编号的映射)组成了一个字符集

编码方式就是规定字符集中字符映射的整数,应该如何在计算机中以二进制的形式存储或传输

Unicode历史

码点是字符集被编码后出现的概念,字符集中每一个字符都和一个编号对应,这个编号就是码点

码元

码元是指一个已编码的文本中具有最短的比特组合的单元,也就是不同的编码规则在编码时所使用的最小单位。

  • UTF-8 以一个字节为最小单位进行编码,所以 UTF-8 的码元为 8 bits

  • 而对于 UTF-16 来说,码元则是16 bits

字符表示

根据 Unicode 规范可以使用 U+ 前缀加上一个十六进制整数表示一 Unicode 个字符

比如: 字符 A 可以使用 U+0041 表示

这种表示方式通常称为 Unicode 转义序列(Unicode Escape Sequence)或 Unicode 码点表示法

不同语言中使用的 Unicode 转义序列语法可能会有所不同:

  • JavaScript 等编程语言中使用 \u + 十六进制整数

    java
    String character = "\u0041"; // 字母A的Unicode码点 String character = "\u4E2D"; // 汉字"中"的Unicode码点
  • HTML 和 CSS 中使用 &#x十六进制数&#十进制数

    html
    &#x0041; <!-- 字母A的Unicode码点 --> &#x4E2D; <!-- 汉字"中"的Unicode码点 -->

平面映射

Unicode 字符集的码点范围为 U+000000 - U+10FFFF(共 1112064 个码点,最多占据 21 bits),一共被划分为 17 个平面,每个平面都包含 65536 个码点,每个平面的码点范围为 U+xx0000 - U+xxFFFF

所谓 Unicode 平面就是字符集中的一个逻辑分组(编码范围),其作用是为了提供足够的码点范围

  • 第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP)或第零平面(U+0000U+FFFF

    • BMP 平面是最常用的平面,包含大多数常用的字符(英文、希腊语、中文、日文、韩文等)
    • BMP 平面内从 U+D800U+DFFF 之间的码点是永久保留不映射到 Unicode 字符
  • 其他 16 个平面称为辅助平面(Supplementary Planes)

    • 第 1 平面包含很多历史字符,以及 emoji 字符
    • 第 2 平面包含了很多汉字
    • 第 15、16 平面用于私有空间

Unicode 最初并没有定义 17 个平面,Unicode 1.0 只定义了基本多文种平面,涵盖了最常见的字符范围

随着 Unicode 的普及和应用,人们对于字符集的需求不断增长,需要更多的码点来表示全球范围内的字符和符号。为了满足这个需求,Unicode 逐步引入了额外的平面,以扩展字符集的能力。

Unicode提供了4种规范化形式

编码方式

有了字符集之后,如何使用二进制存储和传输 Unicode 非常重要,也就是如何对 Unicode 字符集编码。

Unicode 字符集的编码叫做 Unicode Transformation Format,简称 UTF

目前 Unicode 官方支持的编码方式有三种:UTF-8UTF-16UTF-32

UTF-8

UTF-8 是使用最广泛的 Unicode 编码方式,使用可变长编码方式,完全兼容 ASCII,一个字符占据 1-4 字节。

编码规则

  • 编号为单字节的字符,编码字节的第一位为0,后面的7位为这个字符的 Unicode 编码,因此兼容 ASCII
  • 编号为n字节的字符,第一个字节的前n位为1,第n+1位为0,后面字节的前两位都为10,剩余的二进制为该字符的 Unicode

Unicode 码点范围对应的 UTF-8 二进制格式

码点范围(对应的十进制数)二进制格式
U+000000 - U+00007F (0-127)0xxxxxxx
U+000080 - U+0007FF (128-2047)110xxxxx 10xxxxxx
U+000800 - U+00FFFF (2048-65535)1110xxxx 10xxxxxx 10xxxxxx
U+010000 - U+10FFFF (65536-1114111)11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

通过具体的 Unicode 编码,使用 UTF-8 编码方式进行编码:

  1. 找到该 Unicode 编码的所在的编号范围,进而找到与之对应的二进制格式
  2. Unicode 编码转换为二进制数,去掉最高位的0
  3. 将二进制数从右往左一次填入二进制格式的 X 中,如果有 X 未填,就设为 0

例如: “” 的 Unicode 码点是 0x9A6C

  1. 其整数编号是 39532,对应的二进制为 1001 1010 0110 1100
  2. 该字符在第三个编码范围内,UTF-8 编码格式是 1110xxxx 10xxxxxx 10xxxxxx
  3. 将二进制数填入 x 中,编码结果为 11101001 10101001 10101100

UTF-16

UTF-16 也是变长编码方式,一个字符占据 2 / 4 个字节大小

编码规则

  • 使用 2 个字节表示 BMP 平面字符(U+0000 - U+FFFF
  • 使用 4 个字节表示辅助平面字符(U+010000 - U+10FFFF

BMP 平面字符

BMP 平面的字符的码点最多占据 16 bit 也就是 2 bytes

对于 BMP 平面的字符,UTF-16 就是直接使用码点对应的二进制来表示

例如:a 字符对应的 ASCII 值为 97,其 Unicode 码点为 U+0061

所以 UTF-16 就直接使用 0x0061 来表示该字符,也就是 00000000 01100001

辅助平面字符

Unicode BMP 平面中 U+D800 - U+DFFF 之间的码位区块是永久保留不映射到 Unicode 字符

UTF-16 利用该范围的码点来对辅助平面的字符的码位进行编码

具体的编码算法

辅助平面的码点范围 U+010000 - U+10FFFF

  1. 将该字符的码点减去 0x10000(得到的值的范围为 0x0 - 0xFFFFF,最多占 20bit

  2. 将结果补充为 20bit 再将其分为两部分:高位的 10bit 和低位的 10bit,值的范围为0x0 - 0x3FF

  3. 10bit 的值加上 0xD800 得到第一个码元(值的范围是 0xD800 - 0xDBFF),称作高位代理

  4. 10bit 的值加上 0xDC00 得到第二个码元(值的范围是0xDC00 - 0xDFFF),称作低位代理

将获得的高位代理和低位代理结合起来得到的四个字节,就是最终的 Unicode 编码

反过来辅助平面字符的码点可以通过以下公式计算得到

码点 = ((高位代理 - 0xD800) * 0x400) + (低位代理 - 0xDC00) + 0x10000

Surrogate Pairs

高位代理和低位代理组成了 Unicode 代理对(Surrogate Pairs

Surrogate Pairs 也就是 UTF-16 用于表示辅助平面字符的方法

例如:字符 𐑀 的码点为 U+10440

  1. 0x10440 - 0x10000结果为 0x00440,也就是 0000 0000 0100 0100 0000
  2. 10bit 和下 10bit 分别为:0000000001000100000
  3. 高位代理: 0x0001 + 0xD800 = 0xD801
  4. 低位代理: 0x0040 + 0xDC00 = 0xDC40

所以字符 𐑀UTF-16 编码是 0xD801DC40,即 11011000 00000001 11011100 01000000

UTF-32

UTF-32 编码方式每个字符占 4 个字节,直接使用字符对应的码点的二进制形式进行编码

该编码方式占用的储存空间较多,所以使用较少

例如: “” 的 Unicode 码点是 0x9A6C

转化为二进制:1001 1010 0110 1100,这就是它的 UTF-32 编码

JavaScript中的字符串

JavaScript 字符串使用了两种 Unicode 编码混合的策略: UCS-2UTF-16

字符串的 length 属性返回的是字符串中码元的数量,UTF-16 码元为 2 比特而每个字符占据 2 \ 4 比特

因此 length 返回的值可能与字符串中 Unicode 字符的实际数量不匹配(当字符的码点超出 BMP)

javascript
'😀'.length // 2

可以借助迭代器得到正确的字符数量

javascript
const len = (str) => [...str].length len('😀') // 1

早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字。我们将一个16位的二进制编码叫做一个码元(Code Unit)。

后来,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(Code Point)。

ES5 中字符串的 API 都是基于码元的,当 UTF-16 扩展了之后对于辅助平面的字符就会存在问题

ES6 提供了

  • codePointAt 根据字符串码元的位置(字符位置)得到其码点
  • normalize 按照指定的一种 Unicode 正规形式将当前字符串规范化
AuthorPosted onUpdated on
0x1461A02023-06-042023-06-11

本文使用CC BY-NC-SA 4.0创作共享协议,转载请署名,图片请转存。

本文最后更新于 433 天前,文中所描述的信息可能已发生改变