《JavaScript 中精度问题以及解决方案》

JavaScript 中的数字按照 IEEE 754 的标准,使用 64 位双精度浮点型来表示。其中符号位 S,指数位 E,尾数位M分别占了 1,11,52 位,并且在 ES5 规范 中指出了指数位E的取值范围是 [-1074, 971]。


精度问题汇总

想用有限的位来表示无穷的数字,显然是不可能的,因此会出现一些列精度问题:

  • 浮点数精度问题,比如 0.1 + 0.2 !== 0.3
  • 大数精度问题,比如 9999 9999 9999 9999 == 1000 0000 0000 0000 1
  • toFixed 四舍五入结果不准确,比如 1.335.toFixed(2) == 1.33

浮点数精度和 toFixed 其实属于同一类问题,都是由于浮点数无法精确表示引起的,如下:

(1.335).toPrecision(20);    // "1.3349999999999999645"

而关于大数精度问题,我们可以先看下面这个代码片段:

// 能精确表示的整数范围上限,S为1个0,E为11个0,S为53个1
Math.pow(2, 53) - 1 === Number.MAX_SAFE_INTEGER    // true
// 能精确表示的整数范围下限,S为1个1,E为11个0,S为53个1
-(Math.pow(2, 53) - 1) === Number.MIN_SAFE_INTEGER    // true
// 能表示的最大数字,S为1个0,E为971,S为53个1
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE    // true
// 能表示的最接近于0的正数,S为1个0,E为-1074,S为0
Math.pow(2, -1074) === Number.MIN_VALUE // true

通过以上可以明白,[MIN_SAFE_INTEGER, MAX_SAFE_INTEGER] 的整数都可以精确表示,但是超出这个范围的整数就不一定能精确表示。这样就会产生所谓的大数精度丢失问题。

解决思路

首先考虑的是如何解决浮点数运算的精度问题,有 3 种思路:

  • 考虑到每次浮点数运算的偏差非常小(其实不然),可以对结果进行指定精度的四舍五入,比如可以parseFloat(result.toFixed(12));
  • 将浮点数转为整数运算,再对结果做除法。比如0.1 + 0.2,可以转化为(1*2)/3
  • 把浮点数转化为字符串,模拟实际运算的过程。

先来看第一种方案,在大多数情况下,它可以得到正确结果,但是对一些极端情况,toFixed 到 12 是不够的,比如:

210000 * 10000  * 1000 * 8.2    // 17219999999999.998
parseFloat(17219999999999.998.toFixed(12));    // 17219999999999.998,而正确结果为 17220000000000

上面的情况,如果想让结果正确,需要 toFixed(2),这显然是不可接受的。

再看第二种方案,比如 number-precision 这个库就是使用的这种方案,但是这也是有问题的,比如:

// 这两个浮点数,转化为整数之后,相乘的结果已经超过了 MAX_SAFE_INTEGER
123456.789 * 123456.789     // 转化为 (123456789 * 123456789)/1000000,结果是 15241578750.19052

所以,最终考虑使用第三种方案,目前已经有了很多较为成熟的库,比如 bignumber.jsdecimal.js,以及big.js等。我们可以根据自己的需求来选择对应的工具。并且,这些库不仅解决了浮点数的运算精度问题,还支持了大数运算,并且修复了原生toFixed结果不准确的问题。


题外话

还有另外一个与 JavaScript 计算相关的问题,即 Math.round(x),它虽然不会产生精度问题,但是它有一点小陷阱容易忽略。下面是它的舍入的策略:

  • 如果小数部分大于 0.5,则舍入到下一个绝对值更大的整数。
  • 如果小数部分小于 0.5,则舍入到下一个绝对值更小的整数。
  • 如果小数部分等于 0.5,则舍入到下一个正无穷方向上的整数

所以,对 Math.round(-1.5),其结果为 -1,这可能不是我们想要的结果。

当然,上面提到的 big.js 等库,都提供了自己的 round 函数,并且可以指定舍入规则,以避免这个问题。

 

笔记

关于 JavaScript 浮点数计算精度不准确问题

今天在计算总价格(单价*数量)时发现关于 JavaScript 浮点数计算精度不准确问题。以前在做项目的时候也发现过这个问题,当时是加法运算所有给每个数都乘以100,在把两个数的和除以 100,能够解决这个问题,但是尤其在乘法运算时这个计算精度不准确的问题很多。上面那个方法已经解决不了了。

如下所示:

0.1+0.2
0.30000000000000004
(0.1*100+0.1*100)/100
0.2  //加法好使
0.11*1.1
0.12100000000000001
(0.11*100*1.1*100)/10000
0.12100000000000002   // 乘法不好使

产生浮点数计算精度不准确的原因: 在计算机角度,计算机算的是二进制,而不是十进制。二进制后变成了无线不循环的数,而计算机可支持浮点数的小数部分可支持到52位,所有两者相加,在转换成十进制,得到的数就不准确了,加减乘除运算原理一样。

十进制     二进制 
0.1      0.0001 1001 1001 1001 ... 
0.2      0.0011 0011 0011 0011 ... 
0.3      0.0100 1100 1100 1100 ... 
0.4      0.0110 0110 0110 0110 ... 
0.5      0.1 
0.6      0.1001 1001 1001 1001 ... 

所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100110011001100 因浮点数小数位的限制而截断的二进制数字,这时候,我们再把它转换为十进制,就成了 0.30000000000000004

解决方案:

方法一: 通过 toFixed(num) 方法来保留小数。因为这个方法是根据四舍五入来保留小数的,所以最后的计算结果不精确。

(1.0-0.9).toFixed(digits)  // toFixed() 精度参数须在 0 与20 之间(digits是精确的小数点后的位数) 
parseFloat((1.0-0.9).toFixed(10)) === 0.1 // 结果为True 
parseFloat((1.0-0.8).toFixed(10)) === 0.2 // 结果为True 
parseFloat((1.0-0.7).toFixed(10)) === 0.3 // 结果为True 
parseFloat((11.0-11.8).toFixed(10)) === -0.8 // 结果为True 
parseFloat((2.22 + 0.1).toFixed(10)) ===2.23 //结果为True 

方法二:把要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,计算完以后再降级。具体的我没有实现过。

方法三: Math.floor( xxx * 10 ) / 10 // 精确到小数点后一位

1.11*10 
11.100000000000001 //精度不准确 
Math.floor(1.11*10)/10 // 精确到小数点后一位 
1.1 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值