Unicode及其编码方式
Unicode
是一种字符编码标准(字符集),用于表示世界上几乎所有的书写系统的字符。它旨在统一字符表示,使得不同计算机系统和应用程序之间能够正确地交换、处理和显示文本数据。
字符集和字符编码
计算机中储存的信息都是用二进制数表示的,而在屏幕上我们直接看到的是一个个的字符
为了能够将人类使用的字符存储在计算机中:
- 给需要表示的每个字符分配一个编号(整数)
- 当需要使用某个字符时直接用这个编号就可以了
一个系统支持的所有抽象字符的集合(字符和其对应编号的映射)组成了一个字符集
编码方式就是规定字符集中字符映射的整数,应该如何在计算机中以二进制的形式存储或传输
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
+ 十六进制整数javaString character = "\u0041"; // 字母A的Unicode码点 String character = "\u4E2D"; // 汉字"中"的Unicode码点
-
HTML 和 CSS 中使用
&#x十六进制数
或&#十进制数
htmlA <!-- 字母A的Unicode码点 --> 中 <!-- 汉字"中"的Unicode码点 -->
平面映射
Unicode
字符集的码点范围为 U+000000
- U+10FFFF
(共 1112064
个码点,最多占据 21 bits
),一共被划分为 17
个平面,每个平面都包含 65536
个码点,每个平面的码点范围为 U+xx0000
- U+xxFFFF
所谓 Unicode
平面就是字符集中的一个逻辑分组(编码范围),其作用是为了提供足够的码点范围
-
第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP)或第零平面(
U+0000
至U+FFFF
)- BMP 平面是最常用的平面,包含大多数常用的字符(英文、希腊语、中文、日文、韩文等)
- BMP 平面内从
U+D800
到U+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-8
、UTF-16
、UTF-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
编码方式进行编码:
- 找到该
Unicode
编码的所在的编号范围,进而找到与之对应的二进制格式 - 将
Unicode
编码转换为二进制数,去掉最高位的0 - 将二进制数从右往左一次填入二进制格式的
X
中,如果有X
未填,就设为 0
例如: “马” 的 Unicode
码点是 0x9A6C
- 其整数编号是
39532
,对应的二进制为1001 1010 0110 1100
- 该字符在第三个编码范围内,
UTF-8
编码格式是1110xxxx 10xxxxxx 10xxxxxx
- 将二进制数填入
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
-
将该字符的码点减去
0x10000
(得到的值的范围为0x0
-0xFFFFF
,最多占20bit
) -
将结果补充为
20bit
再将其分为两部分:高位的10bit
和低位的10bit
,值的范围为0x0
-0x3FF
-
高
10bit
的值加上0xD800
得到第一个码元(值的范围是0xD800
-0xDBFF
),称作高位代理 -
低
10bit
的值加上0xDC00
得到第二个码元(值的范围是0xDC00
-0xDFFF
),称作低位代理
将获得的高位代理和低位代理结合起来得到的四个字节,就是最终的 Unicode
编码
反过来辅助平面字符的码点可以通过以下公式计算得到
码点 = ((高位代理 - 0xD800) * 0x400) + (低位代理 - 0xDC00) + 0x10000
Surrogate Pairs
高位代理和低位代理组成了 Unicode
代理对(Surrogate Pairs
)
Surrogate Pairs
也就是 UTF-16
用于表示辅助平面字符的方法
例如:字符 𐑀
的码点为 U+10440
0x10440
-0x10000
结果为0x00440
,也就是0000 0000 0100 0100 0000
- 上
10bit
和下10bit
分别为:0000000001
、000100000
- 高位代理:
0x0001 + 0xD800 = 0xD801
- 低位代理:
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-2
和 UTF-16
字符串的 length
属性返回的是字符串中码元的数量,UTF-16
码元为 2 比特而每个字符占据 2
\ 4
比特
因此 length
返回的值可能与字符串中 Unicode
字符的实际数量不匹配(当字符的码点超出 BMP)
javascript'😀'.length // 2
可以借助迭代器得到正确的字符数量
javascriptconst len = (str) => [...str].length len('😀') // 1
早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字。我们将一个16位的二进制编码叫做一个码元(Code Unit)。
后来,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(Code Point)。
ES5 中字符串的 API 都是基于码元的,当 UTF-16
扩展了之后对于辅助平面的字符就会存在问题
ES6 提供了
codePointAt
根据字符串码元的位置(字符位置)得到其码点normalize
按照指定的一种 Unicode 正规形式将当前字符串规范化