背景
js精度问题在前端很常见,经常页面遇见一些计算的展示,我们需要考虑js精度,但是为什么会有精度问题,我们应该怎样解决,今天带大家一起学习一下。
什么是精度问题
JavaScript 中的数字按照 IEEE 754 的标准,使用 64 位双精度浮点型来表示。其中符号位 S,指数位 E,尾数位M分别占了 1,11,52 位,并且在 ES5 规范 中指出了指数位E的取值范围是 [-1074, 971]。
浮点数精度问题,比如 0.1 + 0.2 !== 0.3
大数精度问题,比如 9999 9999 9999 9999 == 1000 0000 0000 0000 1
toFixed 四舍五入结果不准确,比如 1.335.toFixed(2) == 1.33
浮点数精度和 toFixed 其实属于同一类问题,都是由于浮点数无法精确表示引起的,如下:
1 | (1.335).toPrecision(20); // "1.3349999999999999645" |
- 关于大数精度问题,我们可以先看下面这个代码片段:
1 |
|
为什么会有精度问题
首先精度丢失问题并不只是在 Javascript 中才会出现,几乎所有的编程语言都采用了 IEEE-745 双精度64 位浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题。
- 小数的精度问题
有些小数部分转为二进制是无限的位数
0.1 -> 0.0001100110011001…(无限)
0.2 -> 0.0011001100110011…(无限)
1 | 0.8125 * 2 = 1.625 | |
得小数部分的二进制为 1101。
但是 IEEE 754 标准的 64 位双精度浮点数最多支持 53 位二进制位,那么浮点数的二进制会被截断,从而导致精度丢失。
- 整数的精度问题
和其他强类型语言不一样,JavaScript 中 Number 类型统一按浮点数处理,存在最小安全数 (-(2^53 - 1),-9007199254740991) 和最大安全数(2^53 - 1,9007199254740991) 。只要超过安全数的范围,那么就会有精度问题。
1 | 173 / 2 = 86 ... 1 |
得整数部分的二进制为 10101101。
可以发现有限十进制小数 0.1 却转化成了无限二进制小数 0.00011001100…,可以看到精度在转化过程中丢失了!
能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中一位小数 0.1 ~ 0.9 当中除了 0.5 之外的值在转化成二进制的过程中都丢失了精度。
解决办法
首先考虑的是如何解决浮点数运算的精度问题,有 3 种思路:
- 考虑到每次浮点数运算的偏差非常小(其实不然),可以对结果进行指定精度的四舍五入,比如可以parseFloat(result.toFixed(12));
- 将浮点数转为整数运算,再对结果做除法。比如0.1 + 0.2,可以转化为(1*2)/3。
- 把浮点数转化为字符串,模拟实际运算的过程。
先来看第一种方案,在大多数情况下,它可以得到正确结果,但是对一些极端情况,toFixed 到 12 是不够的,比如:
1 |
|
上面的情况,如果想让结果正确,需要 toFixed(2),这显然是不可接受的。
1 |
|
所以,最终考虑使用第三种方案,目前已经有了很多较为成熟的库,比如 bignumber.js,decimal.js,以及big.js等。我们可以根据自己的需求来选择对应的工具。并且,这些库不仅解决了浮点数的运算精度问题,还支持了大数运算,并且修复了原生toFixed结果不准确的问题。
- 还有另外一个与 JavaScript 计算相关的问题,即 Math.round(x),它虽然不会产生精度问题,但是它有一点小陷阱容易忽略。下面是它的舍入的策略:
- 如果小数部分大于 0.5,则舍入到下一个绝对值更大的整数。
- 如果小数部分小于 0.5,则舍入到下一个绝对值更小的整数。
- 如果小数部分等于 0.5,则舍入到下一个正无穷方向上的整数。
所以,对 Math.round(-1.5),其结果为 -1,这可能不是我们想要的结果。
当然,上面提到的 big.js 等库,都提供了自己的 round 函数,并且可以指定舍入规则,以避免这个问题。