浮点数和JavaScript的Number

学习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
2
Math.pow(2, 53) === Math.pow(2,53) + 1  // true
Math.pow(2, 53) - 1 === Math.pow(2, 53) + 1 // false

可以理解为一个安全整数不可以被不安全整数表示,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}$),如下图

IEEE-754计算器

可以使用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

https://www.zhihu.com/question/26022206