js小数运算为什么会发生精度丢失?

150 阅读3分钟

119CDEB5-9A57-40B0-A285-918482E11A3A.png

1 浮点数计算

0.1 + 0.2 === 0.3 // false

0.5 + 0.75 === 1.23 // true

0.09 * 10 === 0.9 // false

从数学角度看,上述运算都应该为true,为什么会出现上述现象?

在计算机中,十进制的数字计算都是需要先转换为二进制,用二进制进行计算;

数学领域中存在无限循环小数,但是在计算机中,无限循环/无限不循环小数显然会不利于有限的计算机内存存储,所以就存在精度丢失问题。

2 浮点数转换二进制

了解精度丢失问题,需要先了解浮点数如何转换成二进制。

浮点数转换为二进制分两部分,小数点左边部分和小数点右边部分。

也可以直接使用进制转换工具进行转换:进制转换

2.1 小数点左边部分

按照整数转二进制的方法进行转换:

将整数部分一直除以2并取余数,余数只会是1或0,直到最后除出来等于0

举个例子🌰:计算17的二进制

image.png

可以得出17的二进制为10001。

2.2 小数点右边部分

小数点右边部分采用乘以2取整数的计算方式,比如0.3: image.png

这里可以看到,0.3的二进制出现了循环,也就是0.010010100101001......

最后将小数和整数部分的二进制组合起来即可:1001.010010100101001......

3 存储

上面提到,计算器的内存空间是有限的,那么对于这种无限循环的二进制浮点数,需要有存储长度的限制。

3.1 IEEE 754 标准

js中,数字都是采用双精度浮点数存储的,也就是说有64位来存储一个数字,这64位分别由sign符号(1位)、exponent指数部分(11位)、mantissa尾数(52位)组成;

  • sign符号: 代表正负,正0,负1
  • exponent指数: 有一套计算方法: 需要计算出E并转换为二进制。 image.png
  • mantissa尾数:小数点左边一定为1.余下的数就为尾数

举个例子🌰: 我们计算出17.3的二进制为:1001.010010100101001......

那么他的符号就为0 指数位:因为需要符合1.M的公式,所以需要将小数点往前移3位变成1.001010010100101001...... 根据E=e(3)+1023得出E=1026,1026换算成二进制为10000000010; 尾数:上一步得出了数字1.001010010100101001......,那么他的尾数就为001010010100101001......(52位)

上述就是计算机存储17.3这个浮点数的计算过程;也就是说,浮点数只会保留53位(首位为1),剩下的就精度丢失了

4 计算

这里以加法计算为例子: 0.1 + 0.2

我们直接在控制台输出0.1保留53位的十进制:

(0.1).toPrecision(53)
// '0.10000000000000000555111512312578270211815834045410156'

输出0.2保留53位的十进制:

(0.2).toPrecision(53)
// '0.20000000000000001110223024625156540423631668090820313'

可以看到因为精度丢失,导致这两个数并不准确,两个精度丢失的二进制相加,必定会精度丢失(不准确)

再看看0.3的

(0.3).toPrecision(53)
// '0.29999999999999998889776975374843459576368331909179688'

最后0.1 + 0.2

(0.1 + 0.2).toPrecision(53)
// '0.30000000000000004440892098500626161694526672363281250'

5 0.5 + 0.75

1中提到0.5 + 0.75 === 1.25 这是因为0.5 和 0.75 转换成二进制都是有限位数的,53位可以存储他们,所以不会发生精度丢失:

0.5.toPrecision(53)
// '0.50000000000000000000000000000000000000000000000000000'
0.75.toPrecision(53)
// '0.75000000000000000000000000000000000000000000000000000'
(0.5 + 0.75).toPrecision(53)
// '1.2500000000000000000000000000000000000000000000000000'

6 解法

对于精度丢失计算,可以使用mathjs去解决:

npm i mathjs

utils/math.js

// 新建配置文件mathjs
const $math = require('mathjs');

function comp(_func, args) {
  let t = $math.chain($math.bignumber(args[0]));
  for (let i = 1; i < args.length; i++) {
    t = t[_func]($math.bignumber(args[i]));
  }
  // 防止超过6位使用科学计数法
  return parseFloat(t.done());
}

export const math = {
  add(...args) {
    return comp('add', args);
  },
  subtract(...args) {
    return comp('subtract', args);
  },
  multiply(...args) {
    return comp('multiply', args);
  },
  divide(...args) {
    return comp('divide', args);
  },
};

math.add(0.1, 0.2) // 0.3