老生常谈的问题了,之前查过一次,模糊记得js、java等语言都存在此问题,后来原因又忘了,最近碰到,再补充一下。

计算机如何用二进制表达小数?

乘2取整,直至小数部分为0,然后把取的整数顺序排列

比如:0.8125:

0.8125 2 = 1.625,取1,剩余0.625
0.625
2 = 1.25,取1,剩余0.25
0.25 2 = 0.5,取0,剩余0.5
0.5
2 = 1.0,取1,终于剩余0.0
顺序排列取的整数得到二进制:1101,所以0.8125的二进制表达就是0.1101

那6.8125呢?

补充一下整数转换二进制规则:

除2取余,直到商为0,然后倒着拼接余数

如:6

6 / 2,得3余0
3 / 2,得1余1
1 / 2,得0余1
倒着拼接余数110即为6的二进制
所以综上所述6.8125的二进制位:110.1101

出现误差的原因

例子是完美的,但很多数据的运算并不顺利,比如0.1

0.1 2 = 0.2 取0剩余0.2
0.2
2 = 0.4 取0剩余0.4
0.4 2 = 0.8 取0剩余0.8
0.8
2 = 1.6 取1剩余0.6
0.6 * 2 = 1.2 取1剩余0.2
….
永远不会剩余0,那就是一个无线循环的数0.0001100110011…(0011保持循环)。

这种情况:计算机会根据IEEE 754进行裁剪取整,这就是精度缺失的根本原因,至于截取规则,IEEE 754规定如果溢出的第一位是1,那就加1,导致某些数据截取后比真实大那么一点点,0.1恰巧因最后的溢出是1,被进位。

总之精度缺失的原因来自计算机对无线循环的无奈

总结、解决方案

  • 认识到小数是不靠谱的,开发中避免直接此类判断0.1 + 0.2 === 0.3
  • toFixed也因此有坑:1.005.toFixed(2),返回的是 1.00 而不是 1.01
  • 第三方工具:number-precision,bignumber.js,Math.js等,把数据转换成string进行处理,性能差点