本文会以图文的形式进行说明,并先补充几个知识点,方便之后的理解。
必备小知识
1. 计算机内部以 二进制bit 进行存储,由 0、1 组成。
2. 高位和低位,二进制位从左到右。 (高位)111001101(低位)
3. 8个二进制位bit等于1个字节byte
4. String.prototype.charCodeAt
charCodeAt返回了字符的code point,是一个0到65535之间的整数
codePointAt跟charCodeAt一样,但可以返回超出65535之后的整数
后面的例子不会超出65535所以会用charCodeAt
5. 按位操作符:按位与&
对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0
6. 按位操作符:按位或|
对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0
7. 按位操作符:右移>>
将第一个操作数向右移动指定的位数,向右被移出的位被丢弃,拷贝最左侧的位以填充左侧(如果最左侧是0则填充0,是1则填充1)
8. ASCII、Unicode、UTF-8三者的关系
ASCII是美国制定的一套对英语字符的编码规范,共规定了128个字符编码
Unicode是统一各个国家符号的一套的字符集,兼容了ASCII,因为Unicode只规定了二进制代码,没有规定如何存储
UTF-8则是因为互联网的普及而诞生的一种统一的编码方式,它只是Unicode的实现方式之一
ASCII编码的字符一定是UTF-8编码的字符,反过来就不成立
9. Unicode符号范围与UTF-8编码的关系
UTF-8编码用1~4个字节表示一个符号
--------------------------------------------------------------------
Unicode符号范围 | UTF-8
十六进制 | 二进制
--------------------------------------------------------------------
00000000 0000007F | 0xxxxxxx (此为兼容ASCII,以0开头)
00000080 000007FF | 110xxxxx 10xxxxxx (2个字节) (开头的1数量表示了字节数)
00000800 0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx (3个字节)
00010000 0010FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (4个字节)
--------------------------------------------------------------------
将上面的Unicode符号范围梳理下:
1个字节: 0x00 ~ 0x7F 0 ~ 127
2个字节: 0x80 ~ 0x7FF 128 ~ 2047
3个字节: 0x800 ~ 0xFFFF 2048 ~ 65535
4个字节: 0x10000 ~ 0x10FFFF 65536 ~ 1114111
好了,以上的知识就是后面的必备知识点了,了解之后就开始今天的旅程吧~
Unicode转换
接下来会使用中文 一 来作为例子的输入
第一种方法: 常规的填补方式
第一步自然是获取 一 的Unicode值
const unicode = '一'.charCodeAt() // -> 19968
知道了 一 的Unicode值,根据上面的必备知识第九点就能得出,它在2048~65538的区间,即 一 在UTF-8中占了3个字节
第二步将Unicode值转换成二进制
const binary = unicode.toString(2) // -> 100111000000000
第三步根据第九点,我们知道了「1110xxxx 10xxxxxx 10xxxxxx」,需要把 x 用二进制数 100111000000000填补掉, 根据x的数量(10xxxxxx 6个)从二进制数右侧取出对应的位数,然后依次从低位到高位替换掉,不足的位数要用 0 替换。
100 111000 000000 [一的二进制数]
1110xxxx 10xxxxxx 10xxxxxx [UTF-8 3字节]
||
||
\/
11100100 10111000 10000000
☝️这里补了一个0
// 3个字节: [0x800 ~ 0xFFFF]
if (unicode >= 0x800 && unicode <= 0xFFFF) {
// x有16个,从头部开始补位
const _binary = binary.padStart(16, 0)
// 再利用substr截取对应位即可
const utf8Binary = `1110${_binary.substr(0, 4)} 10${_binary.substr(4, 6)} 10${_binary.substr(10)}`
}
最后我们得到了 11100100 10111000 10000000
这个就是 一 在UTF-8中的二进制存储
上面的方法比较简单和理解,那有没有更高级的方法呢,当然有,那就是第二种方法啦~
那好,第二种方法呢其实会用到必备知识的第5、6、7点对第一种方法做简化,这种方法利用了按位操作符的特性,巧妙的进行了替换
第二种方法: 按位操作
先要知道一个点: 边界值,最小值和最大值,分别是x为0和x为1的情况。把0和1分别带入进去,得到了
1110*0000* 10*000000* 10*000000*
1110*1111* 10*111111* 10*111111*
*括起来的数就是每个字节所能替换的最小值和最大值
了解这个就开始了
我们先给 「1110xxxx 10xxxxxx 10xxxxxx」从左往右编1、2、3号
上面已经计算出了一的二进制数100111000000000
要做的事情无非就是要把二进制数填补到对应的位置上,从低位往高位填补
1号有4个x,2、3号分别有6个x
第一步
先把每个数的位置跟x对其,不够补0,并分别给它们编号a b c
从图上可以很清晰看出a要在1号这个位置上,那就必须将整体往右边移动12位,即0b100111000000000 >> 12(0b为二进制数字面量),b c会被丢弃
代码如下:
const binary = 0b100111000000000
const a = binary >> 12 // -> 0b100
我们现在只关注a
第二步
回到刚才说的边界值,最大值不能超过xxxx=>1111,再结合按位操作符,哪种符号可以将其限制在1111内(包含了1111)
诶,那就是按位与&,可以回到必备知识第五点看下定义。
将x用1替代与a -> 0100作按位与&操作,即0b1111 & 0b100,最后结果还是a,这里可能还看不出来按位与&的"限制"作用,不着急,讲b就会体现出来
代码如下:
// a = 0b0100
a & 0b1111 // -> 0b0100
第三步
要得出1号最终的二进制,这步很关键,最后再来看下1号 1110xxxx和a 0100,因为1110为控制位,只是表达一些信息,不用改变,要替换的也只是x而已,那第二步已经得到了a的值,也不用改变,那这步就是要让这两个位值不能发生改变,并要产生一个新的二进制,那这时候需要上面说的哪几点呢?
聪明的小伙伴应该已经想到了,就要利用最小值以及按位或|,这样就能完整的保留1110和0100,看图
代码如下:
// a = 0b00000100 b-1之间的0其实可以省略
a | 0b11100000 // -> 0b11100100
这样就得到了1号的最终二进制11100100
按位或|在这里充分体现了一个特性任一数值x与0进行按位或操作,得到的就是x,利用这个特性就能理解上面的保留操作
第四步
接下来关注b,从前面的图中可以看出b要在2号这个位置上,那就必须将整体往右边移动6位,即0b100111000000000 >> 6,这里得到的二进制会包含a的部分,但其实并没有什么影响,为什么呢,就要看按位与&大显神威了,来看下面图片
讲两点:
- 两操作数都为
1,结果才是1 - 任一数值与
0作按位与操作,其结果还是0
根据这两点就能发现多出来的a,并没有什么影响,也体现了按位与&的作用,不管另一个操作数多大,都会被限制在最大值 111111 的范围内
代码如下:
const binary = 0b100111000000000
const b = binary >> 6 // -> 0b100111000
b & 0b111111 // -> 0b0111000
第五步
就是重复第三步操作,只不过二进制数字换掉了
代码如下:
// b = 0b111000
b | 0b10000000 // -> 0b10111000
第六步
关注c,3号因为已经在最后面了,所以不需要向右移动,省略第一步操作,后面的第二步、第三步跟b的步骤一样,这里就不画图了,直接上代码
代码如下:
const binary = 0b100111000000000
const c = binary // -> c不需要移动就等于binary
// 0b100111000000000 & 0b111111
c & 0b111111 // -> 0b000000,就是0
0 | 0b10000000 // -> 0b10000000
完整代码:
const str = '一'
const unicode = str.charCodeAt()
// 这个转换成二进制部分可以省略掉,在进行下面的按位操作时js运行时会把十进制数自动换算成二进制数
// const binary = '0b' + unicode.toString(2)
const result = []
if (unicode >= 0x800 && unicode <= 0xFFFF) {
result.push(unicode >> 12 & 0b1111 | 0b11100000) // 1号 - a
result.push(unicode >> 6 & 0b111111 | 0b10000000) // 2号 - b
result.push(unicode & 0b111111 | 0b10000000) // 3号 - c
}
// result -> [228, 184, 128]
// 转换成十六进制 -> [e4, b8, 80]
// 利用encodeURI来验证下
encodeURI('一') // -> %E4%B8%80
// 发现e4==E4 b8==B8 80==80
// 其实encodeURI将字符编码成UTF-8的,但仅限于特定字符,像汉字
上面的二进制数不好书写,也不利于阅读,我们将其换算成十进制,就是其他文章里的代码了,你一开始看到这个是不是很懵逼,经过上面的过程会不会清晰很多~
const str = '一'
const unicode = str.charCodeAt()
const result = []
if (unicode >= 0x800 && unicode <= 0xFFFF) {
// unicode >> 12 & 0x0F | 0xE0 [十六进制也可以]
result.push(unicode >> 12 & 15 | 224)
result.push(unicode >> 6 & 63 | 128)
result.push(unicode & 63 | 128)
}
上面的Unicode转换带你了解了按位操作符的奇妙之处,但也只是其中的冰山一角。如同我们自己身处在冰山一角内,只有破冰而出,才能看到更广阔的世界,共勉💪!!!
其实上面的转换只讲了3字节,其他的2字节、4字节都是差不多的,感兴趣的同学可以自己上手操作一下~
PS. 作为掘金小透明第一次分享文章,真的很激动,如果文章部分有错误的地方欢迎各位大佬指出,如果你有点收获,非常高兴^ ^~
本文使用 mdnice 排版