本节介绍数字在计算机中如何存储,以及 JavaScript 语言在处理数字时存在的常见问题。
JS 中存在很多问题,首先,JS 中的小数运算有时候是不精准的,读者可以尝试 0.1+0.2 ,得到的结果并不是 0.3,而对于过于庞大的数字,整数运算也会是不精准的。这样数字运算不精准的问题,看似是一个很致命的问题,但实际上并不致命,对于整数而言,虽然大整数的运算可能并不精准,但实际上我们程序中很少会出现这么庞大的数字,所以这个问题几乎可以无视。而对于小数而言,像 0.1+0.2=0.30000000000000004,我们会发现差距非常小,对吧,所以在大多数情况下,这样小的细微差距也不会影响到我们的程序开发。但是问题就出现在比较上,毕竟 0.30000000000000004≠0.3,所以在 JS 中,我们会发现 0.1+0.2==0.3 结果为 false,这就是一个问题。所以综上,在对精度要求很高的情况,或者要对小数的运算结果进行比较的情况下,需要特别注意这个问题。
现在我们知道,JS 不仅存在小数的精度问题,还存在大数不精准的问题。当用 JS 表示一个很大的数字的时候,数字甚至都不连续,我们可以通过 Number.MAX_SAFE_INTEGER 来查看这个最大安全的整数,我们可以简单认为,这就是我们可以使用的最大整数,数值为 9007199254740991,只要我们使用数字的绝对值小于等于这个数,那么我们都可以放心大胆地使用。但如果大于这个数,就会出现问题,我们尝试这样一个运算,9007199254740991+1==9007199254740991+2,我们发现结果为 true,这显然不对,这是因为当 JS 中的整数超过了 Number.MAX_SAFE_INTEGER 之后,整数将不再连续,将会出现跳跃。综上,JS 中过于庞大的数也会产生精度问题,而 JS 中数字的有效位大概是 16 到 17 位。
在实际程序开发中,我们很少会用到这种千万亿级别的数字
接下来解释以上问题发生的原因。
任何数据,在计算机内部都是以二进制的数字存储的。至于十进制和二进制的转换问题这里不过多涉及,如果读者有疑问,可以参考其他教程。
如果读者学习过其他强类型语言,会知道,一个变量必须有一个数据类型,例如整型,浮点型,而且整型变量只能存放整数和浮点型变量则用于存放小数,但在 JS 中我们并不关心数字的类型,不用在意它是整型还是浮点型,或者说它是整数还是小数。实际上,在 JS 中,所有的数字都按照浮点型存放,即都按照小数来存放,也就是说虽然一个变量存储的看起来是一个整数,但实际上它是一个小数,只不过小数部分都是 0。
JS 使用 IEEE 754 标准来存放数字,使用该标准中的双精度(64 位)来存放数字。接下来简单介绍如何存放。
JS 中存储一个数字固定为 64 位,也就是 8 个字节的空间(1 个字节 8 位)。在该标准中,实际上使用科学计数法来表示数字,64 位被分为了三段,分别是 [1][2~12][13~64],第一位表示符号位,1 表示负数,0 表示正数。第 2 到 12 位为指数位,也就是科学计数法中的指数,这里指数一共 11 位,可以表示 0 到 2047,将这个数字减去 1023,就可以得到科学计数法的指数部分,剩下 13 到 64 位,一共有 52 位,用于表示有效数字,我们称为尾数,这个有效数字是小数位,整数固定为 1。
举个例子,假设符号位为 1,2 到 12 位为 11,13 到 64 位为 1010,那么表示的数字就是 , (这里后缀带 B 的都表示二进制)
特殊情况:
- 指数为 0,尾数为 0,表示数字 0
- 指数为 2047,尾数为 0,表示无穷
- 指数为 2047,尾数不为 0,表示 NaN
可见,指数虽然可以表示 2047,但实际上只能取到 2046,如果真的取到了 2047,就会出现问题
为什么 JS 中小数运算不精准?
因为小数部分在转换为二进制之后可能是无限小数。例如十进制下的 0.1 和 0.2 转为二进制,都是一个无限循环小数。而对于无限循环小数,因为它是无限的,计算机不可能无限的存储下去,一定会限制只存储多少位,所以这种存储本身就是不精确的,这就导致了小数运算不精确的问题。这个问题更常见的名字可以叫做精度丢失,这个问题并不是 JS 本身的问题,在其他编程语言也存在这个问题。
为什么 JS 中大整数也会不精准?
JS 中整数也按照浮点数的规则来进行存储,所以当整数过大的时候,也会出现和小数同样的问题。
JS 能表示的最大数字
符号位为 0,指数部分为 2046,尾数全为 1,算出来的结果为 1.7976931348623157e+308,我们可以通过 Number.MAX_VALUE 来查看。
最大安全整数
最大安全整数,其实就是不看指数,指数就是 0(减去 1023 之后),尾数从 0 到最大值,我们知道尾数有 52 位,也就能表示 ,也就是 9007199254740991,这和 Number.MAX_SAFE_INTEGER 所存储的值一样。