而今天要解决的问题也只有一个
utf8、utf16、utf32 都是什么鬼!
和 utf8 等相关的 就是 Unicode,所以今天我们需要先请 Unicode 出场
Unicode
Unicode 是一个字符集,收录了世界上所有的文字符号,并且给这些个文字符号一个唯一的 ID
这样全世界的机器都使用这个字符集,那么就不会出现乱码了
那么 Unicode 肯定是一个非常大的字符集合了,现在大概收集了一百多万个字符
那么就有一个问题
Unicode 怎么给字符分配唯一 ID?
Unicode 使用了 16 进制来给字符串分配ID,并且在前面加上 U
所以说每一个符号都有一个 16进制编号
Unicode给这串ID起了个名字叫[码点]
比如下面的这些字符和他们的Unicode
代码语言:javascript复制U 0020 ,空格U 0030 ,数字0U 006F ,字母oU 007E ,波浪纹 ~
记住哦,这个U 后面就是该字符的16进制编号
然后,Unicode 只给字符规定了它的 编号ID,但是却没有规定它怎么存在计算机中。
怎么存是什么意思呢?
就是给每个字符分配多大的空间去存,准确说分配多少个字节去存
那是谁去确定怎么存的呢?
那就是 utf8、utf16 、utf32 做的事情了,他们各自都有不同的规则去存储字符
比如会存在下面的对话
UTF-8
我规定一个字符存1个字节就好了
我不行,我偏要一个字符存两个字节
UTF-16
但是现在先不急讲规则,我们先来讲一下其他的
怎么确定存一个字符需要几个字节?
看下基本的概念
位(bit)是计算机存储的最小单位,1110 ,一个四 位 的 二进制
字节(byte),数据处理基本单位,大写 B,1B = 8bit
所以,一个字节是由 8个二进制位构成的,最小就是 00000000,最大就是 11111111
一个字节所能表示的最多字符数就是 2的 8次方(8个二进制位),也就是 256个字符
比如 00000001 可以表示一个字符,00000011 也可以表示一个字符,直到 11111111,所以能表示 256 个字符
但是注意了,这里的 一个字节表示256 个字符,不是说 一个字节 可以存256 个字符。一个字节只能存一个字符
只是这个字节,它可以存放的是 A,也可以存放的是 B 等其他单个字符而已(这里一开始我也是懵逼的)
那么现在我们要确定的是 一个字符 要多少个字节存放
已知字节是 二进制的,而Unicode 也确定了每个字符的 16进制编号
所以现在我们需要把 16进制转成 二进制 就能知道 一个字符要多少个字节了
为什么从16进制编号转成2进制,就能知道需要多少字节?
首先,一个字节,有八个二进制位,那么两个字节,就是16个二进制位
现在我把一个16进制编号转成 二进制
比如现在转化后的二进制是 1111(四个二进制位),8>4,那么既然我四个二进制位就能表示这个字符了,那么当然我只需要一个字节就能存放
但是如果转化后的二进制是 1111 1111 1111(12个二进制位),16>12>8,超过了8个二进制位,那么我就要两个字节才能存放了
以此类推更多二进制位......
现在回来,把我们的 Unicode 16进制编号 转成 2进制!!
16进制编号 怎么转成 2进制?
因为这个问题我之前也是不明白的,所以必须记录上来,虽然明白了之后就很简单
所谓 2 进制,就是没有 2,每累计到 2 就进一位
那么 16进制也是一样,累计到 16 就进一位,只不过10 到15 的部分,使用大写字母 A~F 表示
我们先来看 2 进制每一位所表示的 10 进制大小,如下
其实就是 2 的 n 次方,n 从0 开始
16 进制中的F,表示10进制的 15,跟 上面的图对应,15 = 8 4 2 1,所以 F 就是 4个1
然后我们转换的方法是,一分四法,就是 一个16进制数 对应 4个 2进制数
举栗子
比如有一个16进制数是 A42F,拆分成四个 A,4,2,F,然后每个数转 换成 4 位二进制,像这样
A 转成 1010,4 转成 0100
2 转成 0010,F 转成 1111
然后组合起来,二进制就是 1010 0100 0010 1111 ,更直观如下
好的,现在我们已经把Unicode 和 字符的关系讲得差不多了,前面也说了,Unicode 只规定了 字符的编码,但是没有规定 这个字符要存多少字节,而 utf8、utf16、utf32 做的就是这个事了
先给一个获取字符 Unicode 码的函数
代码语言:javascript复制function getUnicdoe(str){ if(!str)return; let unicode = ''; for (let i = 0; i < str.length; i ) { let temp = str.charAt(i); unicode = '\u' temp.charCodeAt(0).toString(16); } return unicode;}
下面我们先说 utf32,再说utf8,再说utf16
UTF-32
UTF-32 规定了每个字符使用四个字节存储,但是这样会十分浪费,因为对于英文等一些简单字符来说,一个字节就能表示了
比如说字母 A就是 0100 0001,只有 8个2进制位,一个字节就搞定了
对于 26个英文字母来说,大小写全算上就是 52个,再加上 10个阿拉伯数字, 62个字符
但是 UTF-32 全使用四个字节去存这些字符的话,就会造成十分大的浪费
如果一个英语文本使用 UTF-32 编码,那么会比 ASCII 码大4倍(ASCII 码是单字节编码),所以不推荐使用这种编码
ASCII 是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。
ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
一种标准的单字节字符编码方案,用于基于文本的数据
UTF-8
utf8 使用了 变长 的编码方式,什么是变长呢,根据字符的不同,可以使用1到4个字节去存储
比如 A 是 0100 0001,那么就使用一个字节就ok 了
而汉字 朱 的编号转成二进制是 0110 0111 0011 0001,那么就使用到两个字节
这样根据字符自动分配合适字节,的确挺好的,但是机器在读取二进制的时候,怎么知道读取多少位是一个字符??
utf32 确定了每四个字节是一个字符,所以很容易区分,但是 utf8 是不确定字节的,机器要怎么知道那部分是一个字符
比如我有一个字符,是 朱A,然后他们的二进制就是
代码语言:javascript复制0110 0111 0011 0001 0100 0001
你让机器怎么知道先读取开头的这些二进制
代码语言:javascript复制0110 0111 0011 0001
翻译为 朱,而不是读取开头的 0110 0111 ?
所以需要定制一套规则,让机器知道哪部分到哪部分是一个字符
我们会在每个字符加上相应的标识,让机器知道怎么读取
比如 0 开头就是 一个字节的字符,110 开头就是2字节的字符,1110 开头就是3个字节的字符
具体如下
11字节的字符
规则:以 0开头的
0xxxxxxx,7个 x 表示 有7个有效位, 2的 7次方 = 128,所以最多可以表示 128种字符。
22字节的字符
规则:前8位以110 开头,后面8位以10开头
110xxxxx 10xxxxxx ,11个 x 表示 有11 个有效位,2的 11次方 = 2048,表示 2048种字符,而我们的常用汉字就有 3000多个,这里肯定是不够放的,只好挪到 3字节。
为什么不以10 开头呢?
因为 后面每8位已经规定使用10 开头了,为了避免冲突,所以使用110。
但是为什么后面要使用 10 开头啊,因为为了确定后面哪部分属于这个字符流
这里我称这个 10 为 续流 标识
如果不使用10 开头会怎么样
假设有一个字符是两个字节的,他的二进制是
代码语言:javascript复制0000 1111 1100 1100
使用 utf8 编码之后,变成
代码语言:javascript复制1100 1111 1100 1100
机器读取到开头 110 ,知道这是一个两字节,然后继续读,mmp,又读到一个 110,虽然这是上一个字符的一部分,但是机器不知道啊
只好当做新字符处理了,而我们不能让机器这么做啊,只能加一个 续流 标识了
33字节的字符
规则:前8位以110 开头,后面每8位以10开头
1110xxxx 10xxxxxx 10xxxxxx,16个 x表示 有16个有效位, 2的 16次方 = 65536 ,最多可以表示 65536个字符,所以我们的汉字就放在这一区,所以在 UTF-8方案里我们的汉字都是以 3个字节表示的。
4简单列一下
0xxxxxxx:单字节编码形式,这和 ASCII 编码完全一样,因此 UTF-8 是兼容 ASCII 的;
110xxxxx 10xxxxxx:双字节编码形式
1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式
以此类推
记住,utf8 使用变长的编码方式给不同的字符分配不同的字节数量,并且给二进制加上了字节数量标识 续流标识
其实 utf8 也挺浪费的,因为编码中占了很多用于标识的无效位
UTF-16
utf16 的内容研究了我三四天,才终于把逻辑弄通了
utf16 是 utf32 和 utf8 的中间产物,结合 定长和 变长 两个编码特点
规则是,基本平面的字符使用 2个字节,辅助平面字符使用 4个字节
也就是,utf16 使用 2 或者 4 个字节 去编码所有字符
基本平面内的字符的范围在 0x0000 到 0xFFFF,使用两个字节
辅助平面内的字符范围是 0x010000 到 0x10FFFF,使用四个字节
你可能会怀疑,0x010000 到 0x10FFFF 不是只占用3个字节吗?
为什么要使用4个字节,因为utf16的编码规则,需要占用多一个字节的空间,你可以当它是占位符吧
接下来先简单说一下规则,再说一下其中我有问题并且逻辑探索的过程
基本平面的字符使用两字节,直接翻译成二进制,这里没有问题,简单地说
比如 说朱的 Unicode 编码是 6731,范围在 0x0000 到 0xFFFF,是基本平面,使用两字节
那么 utf16 编码就是 Unicode 编码直接翻译成二进制就行了,110 0111 0011 0001
辅助平面的字符使用四个字节,并且该平面内的字符需要拆分成两个基本平面的字符进行表示
比如 ? 的 Unicode 编码是 1D785,范围在 0x010000 到 0x10FFFF,是辅助平面,使用四个字节
1、编码 1D785 减去 0x10000(范围的最小值),得到 D785
2、D785 翻译成二进制,1101 0111 1000 0101 ,并且最前面拼上 0000 ,变成 0000 1101 0111 1000 0101,需要把这个二进制分为两部分映射到 基本平面上
3、取二进制前10位拼接在 110110 后,取后10位拼接在 110111后
4、得到 utf16 编码, 110110 0000 1101 01 110111 11 1000 0101
5、换算成16进制,就是 0xD835 0xDF85, 这两个编码是必须一起的,不能分开读
没错,就因为这个辅助平面的规则,弄了我好久才算明白,下面来分别解释下其中我的疑惑
1什么是基本平面,辅助平面
基本平面和辅助平面,都是 Unicode 中的一个编码区段
基本平面编码范围,从 U 0000 到 U FFFF, 也称为 基本多语种平面,也就是这里存放的是很多语言的文字,存放的各种语种的区间如下(图片来自百度百科并且自行翻译)
原来 Unicode 原来的 16位 不足以存储其他字符了,在 Unicode 3.1 版本后,设置了16个辅助平面
让 Unicode 可使用空间,从六万多字符到 一百万字符
辅助平面的字符使用 4字节存储
第一辅助平面,存放拼音文字和符号,范围在 U 10000 - U 1FFFD
第二辅助平面,存放 表意文字,范围在 U 20000 - U 2FFFD ,表意文字是一种图形符号,只代表语素,没有音节
比如我们平常使用功能的颜文字和 emoji 表情更多的就不列举了
2为什么辅助平面规则这么复杂,不像基本平面一样直接翻译成二进制
不像utf32,确定 4个字节为一个字符,所以 utf16 和 utf8 有一样的问题,需要指定一个规则,让机器知道 哪里到哪里属于一个字符
但是 utf16 因为确定只用 2 或4 个字节,所以又比 utf8 规则简单一些
只需要让机器知道 什么时读取 2个字节,什么时候读取4个字节 就行了
并且 UTF-16正好利用了代理区来区分什么时候读4个字节,什么时候读2个字节
规则的解释会在下面
3代理区间的0xD800 到 0xDBFF 空间大小为什么是 2^10 ?
这个问题不难,只是有时绕不过弯
如下图,一共16位,这个区间内,前面6位一直固定是 1101 10
所以只剩下十位是变化的,所以是 2^10
同样的,DC00 到 DFFF 也是 2^10 位
4辅助平面有2^20 个字符,而 D800 到 DFFF 并没有 2^20 位,怎么能映射上去?
不是连续的,是断开的,一起合作的
把一个编号拆分成两部分,读取的时候再合起来读
2^10 映射到 D800 到 DBFF,2^10 映射到 DC00 到 DFFF
这个合作的关系是怎么样的呢?
比如 上面例子说的,编码 1D785 大于 0x10000, 所以 拆成两个 0xD835 0xDF85
第一区间是 D800 到 DBFF,第二个区间是 DC00 到 DFFF
我们以 D800 为例
D800 可以和一整个 第二区间 合作
代码语言:javascript复制0xD800 0xDC000xD800 0xDC010xD800 0xDC02...0xD800 0xDFFF
因为第二区间有 2^10 个,所以上面就有 2*10 个组合
然后第一区间又有 2^10 个,并且每一个都会跟 一整个第二区间 合作
所以就是 2^10*2^10 = 2^20
5减去 0x10000 的二进制为什么要在前面拼接上 0000?
首先,并不是所有辅助平面字符减去0x10000 都需要 在前面拼接上 0000
如果减去 0x10000 之后,二进制的位数不足 20 位,那么我们需要在前面拼接上 0000
因为辅助平面 需要4个字节,一共32位
并且二进制分成两部分拼接头 110110 和 110111,这两个头占了12位
所以字符的二进制位数要保证 20位,少则补
60x10000 以上的字符为什么要减去 0x10000 ?
把一个 大于 0x10000 的字符 ,换算成 两个在某区间内的 值
分成两部分再拼上 110110, 110111
为什么我不能直接拼上 110110, 110111??
你知道的, 0x10000 和 0x10FFFF 的字符,16进制编号必定在第五位是大于等于1 的,如下图
我们取一个最小值来计算一下,就用 0x10000
换成二进制是
代码语言:javascript复制0001 0000 0000 0000 0000
如果直接前10位拼接上 110110, 后10位拼接上110111
就变成
代码语言:javascript复制110110 0001 0000 00 110111 00 0000 0000
换成16 进制就是
代码语言:javascript复制0xD8400 xDC00
哦吼喽,最左边是 0xD8400 啊,你看懂了吗??
我取的是 0x10000 换算的哦
也就是说换算之后的左边最小是 0xD8400!!
明明区间最小是 0xD800 啊
那这样,0xD800 到 0xD8400 这部分就完全没用了啊,这样肯定不行
如果如果,我们先减去 0x10000,换算成二进制,因为位数不够,还要在前面补上 0000,然后拼接好之后的 二进制就是
代码语言:javascript复制110110 0000 0000 00 110111 00 0000 0000
注意,我加重的那一位从 1 变成 0 了
然后转换 16进制就是
代码语言:javascript复制0xD800 0xDC00
嗯,这才是正确的嘛
并且最高的 0x10FFFF 转成二进制有 4*6 = 24 位,而 110110 和 110111 占了12位
那么 24 12 =36>32,已经超出了4字节的位数,需要5字节了
所以减去 0x10000,转成二进制就可以保持为 20位,就刚刚好 4个字节
7为什么要在拆分的两部分二进制前面加上 110110 和 110111 ?
首先,我们拆分的两部分二进制要塞入 这两个不同区间内,我们是不是必须保证大小在这两个区间内??
如下图,两个区间头部都有不变的值,110110 和 110111
并且变化的只有后面10位,所以我们要保证前面不变,后面随便拼接都可以
所以就拼接上110110 和 110111
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵, 如果有任何描述不当的地方,欢迎后台联系本人,领取红包