学习ES6而引起的一系列惨案,因为ES2015定义了一些Number的新属性,如最大安全整数.所以就牵扯到了这个,为什么最大安全整数是Math.pow(2, 53) - 1
,53又是哪里来的?为什么不可以是Math.pow(2, 53)
。下面就根据这几个问题进行解释。
关于浮点数
浮点数分为单精度和双精度。之前在学习C语言的时候了解到,单精度对应的是float类型,双精度对应的是dobule类型,两者的区别就是单精度浮点数在计算机中的存储字节为4个字节也就是32位,双精度浮点数在计算机中的存储字节为8个字节,也就是64位。根据IEEE-754标准,两者都是由三部分组成,且格式如下。
符号位,指数位,尾数位(小数,一般讨论的精度就是指的这个)。浮点数取值范围取决于指数位,计算精度取决于小数位。
符号位(1 bits) | 指数位(8 bits) | 尾数位(23 bits) |
---|---|---|
对于单精度浮点数,符号位所占二进制位为1,指数为8位,尾数(小数)为23位。
符号位(1 bits) | 指数位(11 bits) | 尾数位(52 bits) |
---|---|---|
对于双精度浮点数,符号位所占二进制位为1,指数为11位,尾数(小数)为52位。
指数位的偏差值
对于32位浮点数来说,不考虑负指数的情况下,指数位的8位二进制数可以表示的数值范围为[0,255]
,
因为实际情况是需要考虑负指数和正指数,所以引入了一个偏差值,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值。偏差值为$2^{8-1}-1 = 127$。指数域的取值范围为[-127, 128]。
偏差值的计算公式为
$$K=2^{n-1}-1$$
为什么是这个数和这个公式呢?下面进行分析。
偏差值的计算
首先,如果我们只有4个二进制位来存储数字,我们最多可以存储多少个数字?
首先一个二进制位有两种取值,0或1,那么我们其实就可以存储2^4 = 16个不同的数字。
对于非负整数,其范围就是[0, 15]
对应二进制范围为[0000, 1111]
,
如果考虑到负整数,对于十进制来说其范围可能是[-1, 14]
或 [-7, 8]
或[-8, 7]
等等,而对于二进制,其范围还是[0000, 1111]
。
对于[-7, 8]
,[0000, 1111]
,-7
这里用 0000(2)
来表示,那么0011(2)
表示多少呢? 因为0011
实际上表示的是3, 而前面0000
表示的是-7,那么我们可以推出0011
其实表示的是3-7 = -4
。
这里的7,就是我们所说的偏差值了。同理,对于[-8, 7]
,其偏差值为8。
这个范围区间怎么确定呢?其实是没有一个标准来确定的。
一般情况下是使得非负整数的个数与负整数的个数一样,也就是[-8,7]这种格式,负整数范围为[-8, -1]
, 非负整数取值范围为[0, 7]
,两者都是8个数字。
而对于IEEE-754标准,它采用的是非负整数比负整数多出两个数字的区间范围分配方式,也就是[-7,8]
这种形式的。
对于非负整数和负整数个数相同的区间范围来说,计算偏差值就直接取数字个数的一半就好了,也就是
$$K=2^{n-1}$$
而IEEE-754标准采用的是非负整数比负整数多两个的那种分配方式,所以偏差值的计算就是前面那种偏差值再减去1。
$$K=2^{n-1}-1$$
对于单精度浮点数来说,偏差值为127,所以指数取值范围为[-127, 128]。
如果再去掉全0以及全1的情况,那么就指数范围就只剩下[-126, 127]之间。同理,双精度浮点数偏差值为1023,指数取值范围为[-1023,1024],去掉全0。所以实际的指数值,为指数域中的值减去偏差值
。
IEEE-754标准要求小数点左侧必须为1,所以可以省略小数点前这个1,腾出一个二进制位来保存更多的尾数,所以对于单精度浮点数计算精度可以达到24个二进制位,而对于双精度浮点数计算精度可以达到53个二进制位。
floating number in JavaScript
JavaScrtipt的所有数字都保存为64位浮点数,遵循IEEE-754标准
,JavaScript的安全整数的范围为$-(2^{53}-1),2^{53}-1$ ,我很好奇,那为什么不是$2^{53}$呢?
在Stack overflow上看到有人提问这个问题。
高票回答是这样的。
1 | Math.pow(2, 53) === Math.pow(2,53) + 1 // true |
可以理解为一个安全整数不可以被不安全整数表示,Math.pow(2,53)可以被一个不安全的整数表示,所以其不是安全的.
所以最大安全整数就是Math.pow(2, 53) -1
1
Number.MAX_SAFE_INTEGER === Math.pow(2,53) -1 // true
不过JavaScript的最大整数就是$2^{53}$。
MDN上也阐述了,安全(Safe)在本文中的提到的意思是指能够准确地表示整数和正确地比较整数
1 | Math.pow(2, 53) === Math.pow(2, 53) + 1 // true |
这个结果在数学上是不正确的,所以Math.pow(2, 53)
不是安全的。
最小浮点数
根据IEEE-754标准
符号位为1,尾数位最小一位为1,其他51位为0,指数位为00000000000
($-1023_{10}$),如下图
可以使用Number.MIN_VALUE来得到最小浮点数
最大浮点数
符号位为0,52位尾数都为1,指数位为11111111110
($1023_{10}$) ,不能全为1,如果指数位全为1的话就是正无穷大了。
可以使用Number.MAX_VALUE来得到最大浮点数
Why 0.1 + 0.2 === 0.3 // false
0.1+0.2 的值其实并不严格等于0.3。原因就是JavaScript内部采用的是64位浮点数格式来表示数字,数字参与正常的(+, -, *, /)运算前都是先转化为64位浮点数的格式再进行算术运算。
而0.1, 0.2转化为64位浮点数后参与运算,转化后的64位浮点数是有精度损失的, 因为0.1,0.2和0.3转化为二进制表示时,得到的其实是一个无限小数,而采用IEEE-754
标准去存储的话,只能取到52位小数位,所以,我们需要对尾数进行舍入处理,将其舍入到52位截止,(涉及到了浮点数舍入处理,参考百科),当计算机存储0.3的时候,只舍入了一次,所以精度只损失了一次。而计算机计算0.1 + 0.2 的时候舍入了3次,头两次分别是对0.1, 0.2的舍入, 然后再将结果0.3再进行一次舍入,总共进行了三次舍入,最后的两个0.3的二进制表示并不一样,所以最终表示的结果也就是不同的了。
怎么处理这种情况?
之前做红包小程序的时候,花菜菜就遇到了这个问题,可以到她的博客去看看解决办法.
Thanks
https://medium.com/dailyjs/javascripts-number-type-8d59199db1b6