在编码界一直流传着联通不如移动的一个故事。。。javascript
请不要误会,联通和移动和本篇文章所说的编码确实没什么关系,但请出联通和移动帮忙作个小实验,再来仔细说说编码。java
在Windows系统下,在桌面上右键新建一个记事本文件,打开它输入“联通”两个汉字,Ctrl+S保存并关闭。node
双击再次打开它,看到了什么?奇怪,文字怎么变成乱码了? 函数
好吧,再次新建一个文件,这回输入“移动”保存再试试。神奇,移动竟然完美显示。学习
好了,不说什么故事了,这个有趣的现象正是为了聊聊计算机中“编码”的那些事,以后再解释为何“联通不如移动”。ui
在计算机中,全部存储的数据都由二进制表示。字母、数字、字符这些都不例外,计算机中最小的单位就是二进制位(0和1),8个位表示一个字节,所以8个二进制位就能够排列组合出256种状态,也就是理论上能够表示出256种字符,而由哪些二进制位表示哪些字符,这就是由人来决定的了,也就是人们制定出的各类“编码”。编码
电脑这种东西最先由老外发明,外国人使用的英语只有26个字母,再加上标点、数字和一些符号也不会太多,所以英文一般用ASCII编码来表示。spa
ASCII码最开始只在美国使用,组合出的256种状态中,第0~32中规定了特殊用途,一旦终端、打印机赶上约定好的这些字节被传过来时,就要作一些约定的动做,好比遇到0×10, 终端就换行等等。code
又把全部的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第 127 号,这样计算机就能够用不一样字节来存储英语的文字了。cdn
记得当初学习C语言的时候,就清楚的知道了一些经常使用的ASCII码值,好比大写A是65,小写a是97等。
英文能够表示了,可是世界上除了英文还有不少语言。咱们的中文文字浩如烟海,仅仅靠这8个二进制位远远不够,怎么办?
且不说中文,在欧洲有些国家的语言中也有一些特殊的字母,好比俄文希腊文等。因而便使用127号以后的空位继续表示他们的字母。固然,因为每一个国家的语言不一样,就愈来愈乱,好比130在法语中是字母 é,可是在希伯莱语中130倒是他们的字母 ג。
咱们的中文就更难办了,即便把全部的位都用上,也表示不完成千上万的汉字,因而咱们本身也制定了一套中文的编码GB2312。
中国为了表示汉字,把127号以后的符号取消了,规定:
再后来,发现了GB2312虽然解决了中文编码的问题,可是仍有不足。
GB2312表示的中文有时不够,有些字并非生僻字,可是没有收录其中,当时有个小插曲,我当时在高考报名的系统中查询成绩的时候报不出个人名字,只能报出个人姓,正是由于个人名字“玥”字不在GB2312的编码范围,所以没有。
因而干脆再也不要求低字节必定是 127 号以后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,又增长了近 20000 个新的汉字(包括繁体字)和符号。
这就是更全面的GBK编码。
随着发展,每一个国家都对本身的语言编出一套本身的编码,真是混乱不堪,咱们不知作别人用什么编码,别人也不知道咱们用什么编码,因而标准组织出手了。
ISO标准组织看到了乱象,制定了一套Unicode编码以解决这种混乱的局面,它的制定简单粗暴,不是全世界的语言多么,我干脆就规定,全部的字符都给我用两个字节表示(两个8位一共16位),对于 ASCII 里的那些 半角字符,Unicode 保持其原编码不变,只是将其长度由原来的 8 位扩展为16 位,而其余文化和语言的字符则所有从新统一编码。
从 Unicode 开始,不管是半角的英文字母,仍是全角的汉字,它们都是统一的一个字符。同时,也都是统一的两个字节。
Unicode的制定是在1990年,正式使用在1994年,那个年代在如今来看简直是远古时期,那时因为互联网并不发达并无推广开。
随着互联网的发展,为了解决Unicode传输问题,于时面向众多的UTF标准出现了。
由于UTF8是Unicode的实现方式之一,它们之间是互通的,就是说Unicode编码能够传换为UTF8,它有一套对应规则:
Unicode符号范围(16进制) | UTF8编码(2进制) |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
能够看到,对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。所以对于英语字母,UTF-8 编码和 ASCII 码是相同的(见上面表格的第一行)。
对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一概设为10。剩下的没有说起的二进制位,所有为这个符号的 Unicode 码。
说的有些抽象,举个例子吧,好比来了一个汉字,电脑是怎么知道的它是用UTF8编码的呢?
由于汉字用三个字节表示(别再问为何用三个字节表示了,这是规定),所以第一个字节的前三位都为1,第四位设为0,后面的位都以10开头,因此它确定长这个样子:1110xxxx 10xxxxxx 10xxxxxx。
OK,电脑按照这个规则一看明白了,来的是个汉字!
不如再举个例子,从Unicode编码表中查出一个汉字对应的编码,把它转换为UTF8试一试,就用个人名字“玥”字吧,它的Unicode编码为\u73a5
首先第一步把16进制转换为2进制,它的值是111001110100101
,那怎么拆分这个2进制的值呢?由于UTF8都是后6位为这个字符的Unicode的码,因此咱们从右往左数6位给一一对应上,不足的位补0就行了。
11100111 10001110 10100101
做为开发人员彻底能够用代码实现一下,这里用node.js真实的实现一下转码:
function transferToUTF8(unicode) {
code = [1110, 10, 10];
let binary = unicode.toString(2); //转为二进制
code[2] = code[2] + binary.slice(-6); //提取后6位
code[1] = code[1] + binary.slice(-12, -6); //提取中间6位
code[0] = code[0] + binary.slice(0, binary.length - 12).padStart(4, '0'); //取剩余开始的位,不够补0
code = code.map(item => parseInt(item, 2)); //把字符串转换为二进制数值
return Buffer.from(code).toString(); //利用Buffer转转为汉字
}
console.log(transferToUTF8(0x73a5));
复制代码
运行结果:
玥
复制代码
以上代码定义了一个transfer函数,参数接收一个16进制值,它表明了一个Unicode字符,transfer函数内部先转换为二进制,并按照UTF-8的规则转换为相应的UTF-8编码,最后,利用node.js的Buffer最终转码成汉字,能够看到,已经正确输出了汉字“玥”。
以上,就是简单分析了Unicode和UTF-8的转换关系。
故事就要讲完了,说了这么多编码的事如今能够回头看看开篇为何联通变成了乱码,由于在Windows的记事本中文默认的保存编码为GB2312,经过查询能够查到汉字“联”对应的GB2312编码为\uc1aa,转换为二进制是1100000110101010
,正好是16位两个字节,按8位拆成两组正好与UTF8的第二种编码格式对应上了:110xxxxx 10xxxxxx
,这样再次打开记事本的时候Windows扫描文件内容,它就会认为这是UTF-8编码的文件,而不是GB2312!此时此刻按照UTF-8来解析文件内容固然出现了乱码。
这时能够从新另存为文件,把文件格式改成GB2312来保存,现次打开“联通”终于显示了。
这个例子很极端,能够说“联通”二字的编码正好是个巧合,可是搞明白了编码的细节,更有助于咱们在开发中遇到问题能够快速理解其实质,并加以解决,在此记下笔记,与你们共同窗习提升。