JS浮点精度问题

打开浏览器控制台,进行小数计算

0.1 + 0.2 == 0.3
false
0.1 + 0.2 === 0.3
false
0.1 + 0.2 > 0.3
true
0.1 + 0.2
0.30000000000000004

可以看到,0.1+0.2是不等于0.3,而是大于0.3。

浮点数相加出现这种不相等的情况,是因为其计算过程中存在进制换算问题。

整数转换为二进制

整数换算为二进制是除以2取余,一直除到商为0,然后倒着取余数

比如将24转换为二进制:

//比如24转换为二进制
24/2 = 12余0
12/2 = 6余0
6/2 = 3余0
3/2 = 1余1
1/2 = 0余1

最后倒着取余数,得到24的二进制数

11000

控制台验证一下,没问题。

(24).toString(2)
"11000"

小数转换为二进制

小数换算为二进制则是乘以2取整,最后得到的二进制数是一个无限循环小数,因为计算机存储的空间是有限的,不可能保存一个无限长的数,因此会出现类似”四舍五入“的情况。

比如将0.1转换为二进制:

0.1*2 = 0.2 //无整数,取 0
0.2*2 = 0.4 //无整数,取 0
0.4*2 = 0.8 //无整数,取 0
0.8*2 = 1.6 //有整数,取 1
0.6*2 = 1.2 //有整数,取 1
0.2*2 = 0.4 //无整数,取 0
0.4*2 = 0.8 //无整数,取 0
0.8*2 = 1.6 //有整数,取 1
0.6*2 = 1.2 //有整数,取 1
0.2*2 = 0.4 //无整数,取 0
0.4*2 = 0.8 //无整数,取 0
0.8*2 = 1.6 //有整数,取 1
0.6*2 = 1.2 //有整数,取 1
......

最后正着取数,得到0.1的二进制数(是一个无限循环小数)

0.0001100110011...

控制台验证一下

(0.1).toString(2)
"0.0001100110011001100110011001100110011001100110011001101"

可以看到,按规律,最后一位应该是0。但是在0后的一位是1,那么就往前进一位,类似四舍五入。

因此,JS中0.1取到的二进制数,会比实际上的数要大一点点。0.2转换为二进制也是一样。

计算时,将0.1和0.2转换为二进制数相加,最后将相加的二进制数转换为十进制是0.30000000000000004,当然会比0.3要大了。

JS安全整数

JS中只有一种类型数,即64位(1bit 的符号位,11bits 的指数部分 ,以及52bits 的小数部分)双精度浮点数,当整数数值过大时,就会发生精度丢失。

JS中的安全数

JS中的安全数范围是

(-2**53,2**53)

超过这个范围,就会发生精度丢失,那么计算结果就会不准确。

比如2的53次方结果如下

2**53
9007199254740992

但是

2**53+1
9007199254740992
2**53+2
9007199254740994
2**53+3
9007199254740996

可以看到,计算的结果已经不对了。

JS中能表示的最大最小数

JS中的能表示的最大最小数范围是

(-2**1024+1,2**1024-1)

控制台

2**1024-1
Infinity
//最大数是计算不出来的

//但可以验证它
2**1023*1.999999999999999
1.797693134862315e+308

//小数位再加一个9呢
2**1023*1.9999999999999999
Infinity

toPrecision 和 toFixed

它们的共同点是把数字转成字符串供展示使用。注意在计算的中间过程不要使用,只用于最终结果。

toPrecision 是处理精度,精度是从左至右第一个不为0的数开始数起。

toFixed 是小数点后指定位数取整,从小数点开始数起。

toFixed实际上不是采用的四舍五入,而是四舍六入。有的也叫银行家舍入,简单来说就是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一(ps:这一句网上扒来的)。不过也不完全符合银行家舍入规则,经过实践呢,“四舍六入五考虑,五后非零就进一”这一句是正确的,而“五后为零看奇偶,五前为偶应舍去,五前为奇要进一”这一句就得看浏览器了,在IE11上是正确的而在chrome、firefox、safari、opera上就不对了。

需要注意的是,用 toFixed 来做四舍五入是有bug的。

比如

1.005.toFixed(2)
"1.00"

得到的不是1.01,而是1.00

这是因为1.005 实际对应的数字是 1.00499999999999989,在四舍五入时会被被舍去。

小数如何正确计算

  • 数据展示时

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示

parseFloat((1.4000000000000001).toPrecision(12)) === 1.4
true

这里12为默认精度,可根据需要进行选择。

  • 数据运算时

上面的方法仅用于数据展示时使用,而不能进行数据运算。

若需要进行精确的加法计算,需要将小数转换为整数后再进行运算

function add(num1, num2) {
    //先将小数转换为字符串,然后截取小数部分,计算长度
    const num1Digits = (num1.toString().split('.')[1] || '').length;
    const num2Digits = (num2.toString().split('.')[1] || '').length;
    //Math.pow(4,3)表示4的3次幂 (4*4*4)
    //获取小数部分,取其中最大的数作为指数(也就是获取到最大小数位数)
    const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
    //将要相加的小数乘以上一步取到的数,相加,然后再除回来
    return (num1 * baseNum + num2 * baseNum) / baseNum;
}
分类: JavaScript 标签: 暂无标签

评论

暂无评论数据

暂无评论数据

目录