Skip to content

编码

这里的编码(Encoding)当然不是指简单的编写代码。

为什么会有这个概念呢。

想想古老的印度人是怎么表示数字的吧:用十个不同的符号和位序组合来表示一个确切的数字,演化成今天我们的用阿拉伯数字(Arabic Numerials)表示的十进制系统。

没错就是你身边到处都存在的数字。这套系统只需要简单的十个符号和并不怎么复杂的规则就能从0表示到任意大的确切数字。

我们在第二讲中提到,代码中的表示的“整数”,跟数学意义上的整数,是有所差距的。这个差距其宗之一就在于表示数字的能力上。

比如,我们来计算270

javascript
pow(2, 70);

嗯,结果是什么鬼(1.1805916207174113e+21,特别是后面这段)!

也就是说,我们能直观表示的整数也就21位(十万亿亿级),再之后就只能用科学计数法表示了。

实际你会发现,其实还没到这个数量级的时候,数字就已经无法精确表示了。

为什么会这样呢?

二进制

跟阿拉伯数字系统一样,计算机内部也是利用位序来表示数值,但犹豫数字电路只有通和断两种状态,所以,相当于每位上只能有两种状态,作为传统,我们将用0和1来表示这两种状态。

然后我们就会看到计算机努力的表示数字: 0,1,10,11,100,101,110,111,1000,1001,1010 等你都觉得脑袋大了,计算机才数到10,不过其实关于这个问题并不需要你操心太多,反正跟阿拉伯数字一样,只要足够长,任意大的数都可以表示。

可是我们之前使用的过程告诉我们,其实并不是这样的。为什么呢?

定长

既然计算机表示数字就是靠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个左右,而26<100<27,所以我们只要有长度为7位的二进制串长度就能把美国人民用的那些字符给完全表示出来。

比如我们可以定义,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

十六进制的一位刚好一一对应二进制的每四个位。

于是本来用二进制表示起来很冗长的东西,用十六进制可以比较轻松的表述出来。而且相互之间的转换也很容易。

相反生活中常用的十进制跟二进制对应的就不那么协调,所以一般也比较少用,只要知道对应的位上的值就好。

也就是说,只要能够数

1,2,4,8,16,...,1024,2048,...,32768,65536,131072,...,2147483648

嗯,我觉得如果你玩过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。