JS 小数的精度问题的总结

2024-10-04 22:10:47 浏览数 (3)

精度问题产生的原因

  • 在 JavaScript 中,数字采用 IEEE 754 标准的双精度浮点数(64 - bit)来存储。这意味着数字在内存中的存储是二进制形式。有些十进制小数无法精确地转换为二进制小数,就像 1/3 在十进制下是无限循环小数一样,有些小数在二进制下也是无限循环的。例如,0.1 在二进制下是一个无限循环小数0.00011001100110011...。当计算机存储这个数字时,只能存储一个近似值。

JS 小数的精度问题的总结

经典问题 0.1 0.2 不等于 0.3,都说是精度问题,但这个问题可以再深入一点。

可以从 存储、运算、显示 三个方面来看。

存储

对于计算机,存储下来肯定是 0 和 1,所以我们可以靠 .toString(2) 来进行一个初体验。

代码语言:bash复制
0.2.toString(2) // 0.001100110011001100110011001100110011001100110011001101

0.25.toString(2) // 0.01

上面的例子能看出,0.2 存储时成了循环小数,而 0.25 则没有。

而循环小数不可能一直循环嘛,所以就会存在一定的截断,因此有了精度问题。

以上为二进制的表现,官方则提供了 toPrecision 这个方法供我们了解十进度下的精度表现,更方便理解。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision

代码语言:bash复制
0.2.toPrecision(32) // '0.20000000000000001110223024625157'

0.3.toPrecision(32) // '0.29999999999999998889776975374843'

上面的例子能看出,0.2 在十进制中的存储结果其实比 0.2 大一丢丢,而 0.3 会小。

更为具体的细节,则可去了解 IEEE-754 规范,但有精确到位数的描述。

http://www.softelectro.ru/ieee754_en.html

运算

代码语言:bash复制
0.1.toPrecision(32) // '0.10000000000000000555111512312578'
0.2.toPrecision(32) // '0.20000000000000001110223024625157'
0.1   0.2 // 0.30000000000000004

0.2.toPrecision(32) // '0.20000000000000001110223024625157'
0.3.toPrecision(32) // '0.29999999999999998889776975374843'
0.2   0.3 // 0.5

由此,才可知为何 0.1 0.2 会比 0.3 大,

因为 0.1 和 0.2 的存储结果都比预期的大上一点,所以结果也就大一点。

而 0.2 与 0.3 一个大一个小刚好抵消,所以是符合预期的结果。

至于为什么精确的 0.5 加上不精确的 0.3 结果为精确,那就是位数的问题了

另外,同理,当你使用 toFixed 等官方函数时,也是有类似的精度问题。

代码语言:bash复制
2.55.toPrecision(32) // '2.5499999999999998223643160599750'
2.55.toFixed(1) // '2.5'

1.55.toPrecision(32) // '1.5500000000000000444089209850063'
1.55.toFixed(1) // '1.6'

1.45.toPrecision(32) // '1.4499999999999999555910790149937'
1.45.toFixed(1) // '1.4'

有个题外小故事,为了保持 1234 与 56789 在四舍五入时概率配平,

网传 toFixed 使用的 “银行家算法” 来均匀地分配,但其实按 ECMA-262 标准文档来看并不是哈。

https://tc39.es/ecma262/#sec-number.prototype.tofixed

显示

代码语言:bash复制
2.5499999999999998223643160599750 // 2.55

当你在开发者空间的 Console 去打印时,你会发现它帮忙去掉了精度误差。

避免方案

粗劣的办法,就是将小数转为字符串,以整数的形式去运算再变回小数。

代码语言:javascript复制
 function add(num1, num2) {
         let m = 1000; // 根据小数的精度确定倍数,这里假设小数最多有3位
         let intNum1 = num1 * m;
         let intNum2 = num2 * m;
         let result = (intNum1   intNum2)/m;
         return result;
       }
       let sum = add(0.1, 0.2);
       console.log(sum); 

但上述方案会遇到整数结果过大而超出安全数范围的问题,

那就只能靠一位一位来处理的方式了,也即 decimal.jsbignumber.js 等库的实现方式。

https://www.npmjs.com/package/decimal.js

decimal.js为例:

代码语言:javascript复制
       const Decimal = require('decimal.js');
       let num1 = new Decimal('0.1');
       let num2 = new Decimal('0.2');
       let sum = num1.add(num2);
       console.log(sum.toString());

这些库提供了精确的数字运算方法,通过将数字以字符串形式传入构造函数,在内部以高精度的方式进行运算,能够有效避免 JavaScript 原生数字类型的精度问题。

0 人点赞