Appearance
编码
这里的编码(Encoding)当然不是指简单的编写代码。
为什么会有这个概念呢。
想想古老的印度人是怎么表示数字的吧:用十个不同的符号和位序组合来表示一个确切的数字,演化成今天我们的用阿拉伯数字(Arabic Numerials)表示的十进制系统。
没错就是你身边到处都存在的数字。这套系统只需要简单的十个符号和并不怎么复杂的规则就能从0表示到任意大的确切数字。
我们在第二讲中提到,代码中的表示的“整数”,跟数学意义上的整数,是有所差距的。这个差距其宗之一就在于表示数字的能力上。
比如,我们来计算
javascript
pow(2, 70);
嗯,结果是什么鬼(1.1805916207174113e+21
,特别是后面这段)!
也就是说,我们能直观表示的整数也就21位(十万亿亿级),再之后就只能用科学计数法表示了。
实际你会发现,其实还没到这个数量级的时候,数字就已经无法精确表示了。
为什么会这样呢?
二进制
跟阿拉伯数字系统一样,计算机内部也是利用位序来表示数值,但犹豫数字电路只有通和断两种状态,所以,相当于每位上只能有两种状态,作为传统,我们将用0和1来表示这两种状态。
然后我们就会看到计算机努力的表示数字:
可是我们之前使用的过程告诉我们,其实并不是这样的。为什么呢?
定长
既然计算机表示数字就是靠0和1,那么如果我要表示多个数字,怎么来区分呢?
比如,你知道下面究竟是1,2,3,4还是13, 14?
javascript
11011100
于是其实我们可以思考一下,出了数值,还有什么可以利用的。
长度。(以及其实你也可以用霍夫曼编码。)
当我规定用于表示数据的长度必须是一定(比如4位)的时候,表示数据就特别地清晰了:
javascript
0001 0010 0011 0100
就可以表示1,2,3,4这四个数。而上面那个我们理解不了的序列,也可以解释成13,14这两个数。
当你去规定一种解释二进制序列的方法的时候,其实你就是在定义一种编码。
二进制序列中,每个0或者1,叫一个bit(位),特定长度(一般是8)的基本解释单位,叫做byte(字节),这个特定长度,据说是依据当前系统中表示基本的字符编码的长度来确定。
换句话说,编码不仅仅是能用来表示数字的。
ASCII
现在提到字节大家都会反应是8位,就是因为这个东西。
全名叫American Standard Code for Information Interchange(美国标准信息交换码)。
这套代码从上世纪60年代一致用到现在,足以说明有多么经典。然而我们先不说他是什么,简单的从我们自己的角度出发先设计一套东西。
字符集
要知道美国人民的生活很是单调的,每天就面临大小写加起来四十多个字母,十个阿拉伯数字,以及十多个常用的标点符号。对比起来,深深地为自己的母语感到自豪。
当然,美国人民日常使用的字符量少,在信息化的时代,其实是有优势的,比如,把他们常用的字符加起来也就100个左右,而
比如我们可以定义,0000000 ~ 0001001 表示数字字符0-9, 0100001 ~ 0111010 表示大写字母字符的A-Z,1000001 ~ 1011010表示小写字母字符的a-z,于是我们就能对把对应的字母变成相应的编码了。
但是老是这样看着满屏的0和1,总是会感觉眼花。既然都是表示数据,并不是说相同的东西,计算机看到长什么样子,我们就要看一样的。只要是能够表示相同的数据,完全可以换一种我们更习惯的方式来表示。
十六进制
比如我们可以使用十六进制(Hexadecimal)代替二进制(Binary),也就是说,同一个位上面共有16个状态,习惯上分别用0-9加上a-f来表示,同时,为了与十进制表示进行区分,一般会加上0x前缀或者h后缀。对于刚刚的字符编码来说,就可以用0x00 ~ 0x09 表示 数字字符,0x21 ~ 0x3A 表示大写字母A-Z,0x40 ~ 0x5A表示小写字母a-z。
为什么是十六进制而不是其他的十二进制或者十八、二十四进制呢?
我们可以简单的做一个对比:
Bin 0001 0010 0011 .... 1010 1011 1100 1101 1110 1111
Hex 1 2 3 A B C D E F
Dec 1 2 3 10 11 12 13 14 15
十六进制的一位刚好一一对应二进制的每四个位。
于是本来用二进制表示起来很冗长的东西,用十六进制可以比较轻松的表述出来。而且相互之间的转换也很容易。
相反生活中常用的十进制跟二进制对应的就不那么协调,所以一般也比较少用,只要知道对应的位上的值就好。
也就是说,只要能够数
嗯,我觉得如果你玩过2048的话一定不会对前面的那些数字感到陌生。
ASCII
嗯,ASCII跟我们想的一样,选择了8位作为实际的长度,同时加入了一些必须要用到的特殊字符(比如制表符tab 0x09,回车记号carriage return 0x0D,换行符line feed 0x0A..),以及一些控制字符(比如空字符NUL 0x00,文件结束符EOF等)。于是整体的设计相对于我们的想法有一些偏移:
\x30
~\x39
表示0-9\x41
~\x5a
表示A-Z\x61
~\x7a
表示a-z
好的我们来试试吧:
javascript
'\x41'
我们可以直接用引号'
括起来单个字符,在程序中直接拿来使用。同时也可以反过来看:
javascript
let a_char = 'a';
a_char.charCodeAt(0).toString(16)
CJK字符集
CJK(有时候也会带上V),是China,Japan,Korea(和Vietnam)的统称,在这些地区,因为同遗传自古汉语,字符的数量可是非常之多,8位的ASCII根本就没办法保存这么多东西。
于是随着各种语言诞生了各种编码,比如GB2312
简体中文,Big-5
繁体中文,Shift-JIS
日文等。这些编码并不是相互兼容的,于是一般对于特定的内容,需要专门指出其编码。同样地,当你尝试去一种编码来解释另外一种编码的文本时,就会出现乱码的情况。注意这个时候并不是你电脑中毒了。
Unicode
所以就有人想,不如把所有的编码都统一了吧,于是就出现了Unicode。
现行版本的Unicode加入了世界上能够找到的绝大多数语言的符号(嗯,包括emoji),然后将这些符号分类整理,统一编码。于是,在理想情况下,大家都使用Unicode字符集的话,就不会出现乱码的错误了。
Unicode包含的字符量足够的多,于是就要设计比ASCII更充分大的表示方式才能够容纳所有的符号。最初选择的是每个字符使用16位的二进制串来表示,这样的话一个中文字符虽然会超出8位范围之外,还是能够表示得出来,这种方法被称为UTF-16。
然而不久就发现,符号的数量(就算仅仅是中文字符)远远多于65536个,于是又进一步地扩充到了32位,可以表示到上亿个字符了,目前为止还要很久才会资源枯竭,这种方法类似地,被称为UTF-32。
然而UTF-32有个很致命的缺点。
对于西方人来说,并没有CJKV地区的人的需求,1个字节就能足够表示他们的符号,而UTF-32硬生生地为了统一,要求字母字符也要用4个字节来表示,这样就让空间整整大了4倍,而且另外3倍的空间完全是浪费掉的,没有任何作用。
但是,为了兼容,没办法。
UTF-8
于是为了解决这个浪费空间的问题,UTF-8出现了。这是来自Ken Thompson的设计,他给整个IT界贡献了不少东西,Unix、C语言、UTF-8。
简单来说就是,在保持与ASCII兼容的情况下,通过添加一些标记来支持更长的编码。
具体的做法当然维基百科介绍的比我更清楚,这里提起来只是要说这么一个思路:变长编码(Variable-Width Encoding)。因为比较重要的就是在设计的过程中考虑变量和不变量,以及设置各种标志和不同的数据区块解释方式,这样就可以把简单的二进制序列变成我们想要的任意数据。
当然,这里的思想我们会在第6章详细地展开。
数系的扩充
你有没有尝试计算JavaScript里面1/3
的结果?
当我们需要小数的时候又该如何表示呢?
这个问题留作练习。
作为参考答案,你可以搜索IEEE754。