今天在看《JavaScript高级程序设计》的时候,注意到书中特地提到了0.1+0.2=0.30000000000000004
这样一个浮点数计算错误的问题,以为颇有意思。平时在工做中对于浮点数了解地并很少,正好最近小组同窗也遇到了这个问题,准备来总结下这个看似简单的Number基础类型,其实并不简单。这篇博客意在从这个奇怪的计算结果去学习总结浮点数的相关知识。javascript
Number.MAX_VALUE
和Number.MIN_VALUE
来获得证明Number.MIN_SAFE_INTEGER
和Number.MAX_SAFE_INTEGER
来求证01 + 0.2 //0.30000000000000004
9007199254740991 + 2 // 9007199254740992
想要解释清楚上述的两个事实和问题,须要先知道小数在计算机中是如何存储的:前端
咱们知道,JS中的Number类型使用的是双精度浮点型,也就是其余语言中的double类型。而双精度浮点数使用64 bit来进行存储,结构图以下:java
也就是说一个Number类型的数字在内存中会被表示成:s x m x 2^e
这样的格式。git
在ES规范中规定e的范围在-1074 ~ 971,而m最大能表示的最大数是52个1,最小能表示的是1,这里须要注意:github
二进制的第一位有效数字一定是1,所以这个1不会被存储,能够节省一个存储位,所以尾数部分能够存储的范围是1 ~ 2^(52+1)安全
也就是说Number能表示的最大数字绝对值范围是 2^-1074 ~ 2^(53+971)bash
前面提到,计算机中存储小数是先转换成二进制进行存储的,咱们来看一下0.1和0.2转换成二进制的结果:微信
(0.1)10 => (00011001100110011001(1001)...)2
(0.2)10 => (00110011001100110011(0011)...)2
复制代码
能够发现,0.1和0.2转成二进制以后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,而这个取舍的规则就是在IEEE 754中定义的,0.1最终能被存储的有效数字是函数
0001(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)101
+
(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)01
=
0100(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)111
复制代码
这里注意,53位的存储位指的是能存53位有效数字,所以前置的0不算,要日后再取到53位有效数字为止。工具
最终的这个二进制数转成十进制就是0.30000000000000004(不信的话能够找一个在线进制转换工具试一下。
到此,这个精度丢失的问题已经解释清楚了,用一句话来归纳就是,计算机中用二进制来存储小数,而大部分小数转成二进制以后都是无限循环的值,所以存在取舍问题,也就是精度丢失。
这里直接推荐一篇文章,关于这个问题讲的很是清楚(文中有一处错误,会在下面指出。
若是懒得看英文的话,能够看个人总结:
最大安全整数9007199254740991对应的二进制数如图:
53位有效数字都存储满了以后,想要表示更大的数字,就只能往指数数加一位,这时候尾数由于没有多余的存储空间,所以只能补0。
如图全部,在指数位为53的状况下,最后一位尾数位为0的数字能够被精确表示,而最后一位尾数为为1的数字都不能被精确表示。也就是能够被精确表示和不能被精确表示的比例是1:1
。
同理,当指数为54的时候,只有最后两位尾数为00的能够被精确表示,也就是能够被精确表示和不能被精确表示的比例是1:3
,当有效位数达到x(x>53)
的时候,能够被精确表示和不能被精确表示的比例将是1 : 2^(x-53) - 1
。
能够预见的是,在指数愈来愈高的时候,这个指数会成指数增加,所以在Number.MAX_SAFE_INTEGER ~ Number.MAX_VALUE之间能够被精确表示的整数能够说是百里挑一。
我发现这篇文章中的一个错误,文章中指出9007199254740998这个数字不能被精确表示,其实是能够的,在指数位是53的状况下,偶数能够被精确表示,奇数不能被精确表示,不能被精确表示的最小偶数应该是当指数位为54,而且最后两位尾数为0的时候。
之因此会有最大安全整数这个概念,本质上仍是由于数字类型在计算机中的存储结构。在尾数位不够补零以后,只要是多余的尾数为1所对应的整数都不能被精确表示。
能够发现,不论是浮点数计算的计算结果错误和大整数的计算结果错误,最终均可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。那么咱们在平常工做中碰到这两个问题该如何解决呢?
大而全的解决方案就是使用mathjs,看一下mathjs的输出:
math.config({
number: 'BigNumber',
precision: 64
});
console.log(math.format(math.eval('0.1 + 0.2'))); // '0.3'
console.log(math.format(math.eval('0.23 * 0.34 * 0.92'))); // '0.071944'
console.log(math.format(math.eval('9007199254740991 + 2'))); // '9.007199254740993e+15'
复制代码
其实平时在遇到整型溢出的状况是很是少的,大部分场景是浮点数的计算,若是不想由于一些简单的计算引入mathjs的话,也能够本身来实现运算函数(须要考虑数字是否越界和当数字被表示成科学计数法的场景),若是懒得本身实现的话,可使用这个1k都不到的number-precision,这个工具库API简洁不少,已经能够解决浮点数的计算问题了(看了代码,对于超出Number.MAX_SAFE_INTEGER的数字的处理方式是抛出warning)。
ps:广告一波,网易考拉前端招人啦~~~有兴趣的戳我投简历