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的二进制
可以得出17的二进制为10001。
2.2 小数点右边部分
小数点右边部分采用乘以2取整数的计算方式,比如0.3:
这里可以看到,0.3的二进制出现了循环,也就是0.010010100101001......
最后将小数和整数部分的二进制组合起来即可:1001.010010100101001......
3 存储
上面提到,计算器的内存空间是有限的,那么对于这种无限循环的二进制浮点数,需要有存储长度的限制。
3.1 IEEE 754 标准
js中,数字都是采用双精度浮点数存储的,也就是说有64位来存储一个数字,这64位分别由sign符号(1位)、exponent指数部分(11位)、mantissa尾数(52位)组成;
sign符号: 代表正负,正0,负1exponent指数: 有一套计算方法: 需要计算出E并转换为二进制。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