js负数比较大小_JS的尴尬之计算不精确问题

时间:2024-11-21 09:07:57

点击上方蓝字关注我们

小编提示: 译文由阿里巴巴企业金融事业部-虚痕同学提供,建议收藏阅读,更好了解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.10.2 的总和是否为 0.3,我们会得到 false

奇了怪了!如果不是 0.3,那能是啥?

alert( 0.1 + 0.2 ); // 0.30000000000000004

哎哟!这个错误匪夷所思。想象一下,你在购物网站下单了 ¥ 0.10¥ 0.20 的商品放入购物车。订单总额将是 ¥ 0.30000000000000004。你可能会被这样的订单总额吓到。

为什么会这样呢?

一个数字以二进制的形式存储在内存中,是一个只有 1 和 0 的序列。在十进制数字系统中虽然看起来很简单,但0.10.2 这样的小数,实际上在二进制形式中是无限循环小数。

换句话说,什么是 0.10.1 就是 1 除以 101/10,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:1/3。三分之一变成了无限循环小数 0.33333(3)

在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数。

使用二进制数字系统无法 精确 存储 0.10.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,企业金融,考拉)共同维护的公众号奥,我们会定期发送优质好文,欢迎扫码关注

求关注

求转发