Unicode 和 编码方式的理解

1,946 阅读6分钟

ASCII 码

计算机内部所有信息最终都是一个二进制值。每个二进制位(bit)有 0 和 1 两种状态,因此8个二进制位就可以组合成 2^8 种就是 256 种组合,这被称为1个字节。也就是说,1 个字节可以表示 256 种不同的状态,从 0000000011111111

ASCII 码是一种字符编码,对英语字符和二进制位之间的关系,做了统一规定。其中一共规定了 128 个字符的编码,这 128 个字符只占用了一个字节的后 7 位,规定最前面的一位统一为 0。比如:空格 SPACE 是 32 (二进制 00100000)。

非 ASCII 编码

英语用 128 个符号编码就够了,但是其他语言,128 个字符是不够的。所有有些国家就决定利用闲置的最高位来编入新的符号。所以,不同的国家有不同的字母,最初的 0~127 表示的符号是一样的,但是 128~255 这一段是不一样的。

对于汉字,使用的符号就更多了,一个字节只能表示 256 种符号,所以肯定是远远不够的,就必须使用多个字节来表达一个符号。

Unicode

就是因为上面说的 128~255 这一段有可能是不一样的,世界上存在多种编码格式,同一个二进制数字可以被解释成不同的符号。因此要想打开一个文本文件,必须知道他的编码方式,否则用错误的编码方式解读,就会出现乱码。所以,如果有一种编码,将世界上所有的编码方式都纳入其中,每一个符号都给予一个独一无二的编码,那么乱码问题就会被解决,这就是 Unicode,这是一种所有符号的编码。

Unicode 是一个非常大的集合,Unicode 分区定义,每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有17个(共 2^5, 剩下的没有用)平面。最前面的 65536 个字符位,称为基本平面(缩写BMP),它的码点范围是从 0 一直到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。剩下的字符都放在辅助平面(缩写SMP),码点范围从 U+010000 一直到 U+10FFFF。

每个符号的编码都不一样。具体的符号对应表,可以查询 Unicode中日韩汉字Unicode编码表

它从 0 开始,为每个符号指定一个编号,这叫做"码点"(code point)。比如,码点 0 的符号就是 null(表示所有二进制位都是0)。

U+0000 = null

U+ 后面紧跟的十六进制数是 Unicode 的码点。

需要注意的是,Unicode 只是一个符号集,它只规定了符号的码点,却没有规定码点如何存储。比如,汉字 的 Unicode 是十六进制 4E25,转换成二进制为 100111000100101,也就是说要表示这个符号至少需要 2 个字节,表示其他更大符号,需要更多字节。

那么问题就来了,第一个问题是,如何才能区别 Unicode 和 ASCII,也就是说计算机怎么知道三个字节是用来表示三个符号还是用来表示一个符号;第二个问题就是,ASCII 码用一个字节就能表示所有英文字母了,如果用 Unicode 统一规定,每个符号用三个或者四个四节表示,那么每个英文字母钱必然有 2 到 3 个字节是 0,这对于存储来说是极大的浪费。

因此就出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式来存储码点。

UTF-32 和 UTF-8

UTF-32 使用 4 个字节表示码点,字节内容一一对应码点,完全对应。

U+0000 = 0x0000 0000
U+597D = 0x0000 597D

优点在于转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比 ASCII 码要大四倍(ASCII 码用 1 个字节存储,UTF-32 用 4 个字节存储)。

UTF-8 是一种长度可变的编码方法,它的长度从 1 个字节到 4 个字节不等。越是常用的字符,字节越短,最前面的 128 个字符,只是用 1 个字节表示,跟 ASCII 码完全相同。

UTF-8 的编码规则:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的
  2. 对于 n 字节的符号(n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码

定长编码的好处是可以快速定位字符,对于string.charAt(index)方法有着较好的支持。 UTF-8的话,就需要从头开始一个字符一个字符的解析才行,会慢一点。 但是与查询定位相比,顺序输出的情况更多,所以平常也不会感受到效率会比较慢。

UTF-16

UTF-16 编码介于 UTF-32 和 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。

编码规则:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。

JS 中采用的编码方式 UCS-2

UCS-2 编码使用 2 个字节来表示已经有码点的字符(UCS-2发布时只有一个平面,即基本平面,所有两个字节就够了)。

那 JS 为什么不使用更高级的 UTF-16 而采用 UCS-2 编码呢,因为在 JS 出现的时候,还没有出现 UTF-16 编码。

由于 JS 只能处理 UCS-2 编码,所以造成所有字符在这门语言中都是 2 个字节,如果是 4 个字节的字符,会当作两个双字节的字符处理。所以 JS 中的字符函数都受到这个性质的影响,无法返回正确结果。

console.log('𝌆'.length) // 2
console.log('𝌆' === '\uD834\uDF06') // true
'𝌆'.charCodeAt(0) // 55348
parseInt('D834', 16) // 55348

ES6 中增加了 Unicode 的支持,可以自动识别 4 字节的码点,这里先不做解释。