点击上方蓝字关注我们
小编提示: 译文由阿里巴巴企业金融事业部-虚痕同学提供,建议收藏阅读,更好了解js计算不精准的文同哦
在javascript内部,数字只有一种类型number, 遵循 IEEE-754 规范 属于双精度浮点数,采用二进制格式,占用 64 位存储空间存储。
符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
指数位E:中间的 11 位存储指数(exponent),用来表示次方数
尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零
如果一个数字太大,超出了64 位存储,可能导致无穷大:
alert( 1e500 ); // Infinity
这个例子可能不经常遇到,比较常见的是,精度的损失。
考虑下面的例子:
alert( 0.1 + 0.2 == 0.3 ); // false
如果我们检查 0.1
和 0.2
的总和是否为 0.3
,我们会得到 false
。
奇了怪了!如果不是 0.3
,那能是啥?
alert( 0.1 + 0.2 ); // 0.30000000000000004
哎哟!这个错误匪夷所思。想象一下,你在购物网站下单了 ¥ 0.10
和 ¥ 0.20
的商品放入购物车。订单总额将是 ¥ 0.30000000000000004
。你可能会被这样的订单总额吓到。
为什么会这样呢?
一个数字以二进制的形式存储在内存中,是一个只有 1 和 0 的序列。在十进制数字系统中虽然看起来很简单,但0.1
,0.2
这样的小数,实际上在二进制形式中是无限循环小数。
换句话说,什么是 0.1
?0.1
就是 1
除以 10
,1/10
,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:1/3
。三分之一变成了无限循环小数 0.33333(3)
。
在十进制数字系统中,可以保证以 10
的整数次幂作为除数能够正常工作,但是以 3
作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2
的整数次幂作为除数时能够正常工作,但 1/10
就变成了一个无限循环的二进制小数。
使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
我们可以看到:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。
这就是为什么 0.1 + 0.2
不等于 0.3
。
我们能解决这个问题吗?当然,最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
let sum = 0.1 + 0.2;alert( sum.toFixed(2) ); // 0.30
请注意,toFixed
总是返回一个字符串。它确保小数点后有 2 位数字。如果我们有一个网购网站,并需要显示 ¥ 0.30
,这其实很方便。对于其他情况,我们可以使用一元加号将其强制转换为一个数字:
let sum = 0.1 + 0.2;alert( +sum.toFixed(2) ); // 0.3
我们可以将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。当我们使用整数进行数学运算时,误差会有所减少,但仍然可以在除法中得到:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
因此,乘/除法可以减少误差,但不能完全消除误差。
其实业内已经有相对成熟的组件库可以解决这个问题,这里给大家推荐
0.1 + 0.2 // 0.30000000000000004x = new BigNumber(0.1)y = x.plus(0.2) // '0.3'BigNumber(0.7).plus(x).plus(y) // '1'x.plus('0.1', 8) // '0.225'
本文译至
/number
写在最后
方凳雅集是由阿里巴巴B系6大BU(1688,ICBU,零售通,AE,企业金融,考拉)共同维护的公众号奥,我们会定期发送优质好文,欢迎扫码关注
求关注
求转发