js 浮点计算陷阱(float pitfall)
目录
很多刚学 javascript 的同学可能与有悟一样,都碰到过这样的情况,比如在构建页面时,使用了 javascript 对数据进行计算,比如使用 19.9 - 10
,得到了 9.899999999999999
这个结果,第一反应一般是『我发现了一个 js 重大的 bug』,😭😤☹️😖。接着火狐、chrome、nodejs挨个试验,发现得到的结果都是一样,然后就开始怀疑人生了,『怎么这些环境都是有 bug 的』。想想,如果这个问题是 bug 的话,怎么会轮到被你发现呢?
事实上,你碰到了浮点数陷阱了。如果使用 js 浮点数陷阱、js 浮点数精度、js 计算精度 等等关键字,就可以搜索到一大堆相关的文章。
实际上,这个问题是由于 javascript 中关于数字表达与计算的实现所导致的。首先,它不是 bug,并且遵循了二进制浮点数算数标准(IEEE 754),但在实际应用中会造成开发者的困扰,并且不断地困扰新入行的开发者,所以总是有人发布关于这个问题的文章。
有悟读到几篇文章,它们把这个问题讲得比较清楚,对数学有点儿兴趣的可以看看,其中介绍了 js 中数字表达原理。
这里还有一个演示,查看输入的数字对应的浮点数表示, convert_double
而若要使用几句话简单来概括的话,那么就是,js 中的数字类型,不管是整数还是小数,都只有 Numbers
一种,它使用了64位浮点数(双精度)来表达,所以就有了浮点计算的精度问题。
通常,解决这个问题,有这几种思路:
- 使用js 计算库,这些库使用 js 重新实现了数字类型的数值计算,相当于重新实现了一个计算器
- 使用整数
- 四舍五入,包括尾0、尾9截位
- 使用字符串、对结果进行格式化
- 不在 js 里进行计算
使用数学计算库 #
下面仅作列举之用,是否属于推荐看你自己需要:
数学变换-整数 #
比如,上面的例子 19.9-10
,可以在 devtool 或者 nodejs 里直接试验:
> 19.9 - 10
9.899999999999999
> (19.9*10 - 10*10)/10
9.9
上面的例子,是将参与计算的带小数的数值,乘以10
全部转换为整数,然后计算结果再除以10
把单位换算来回。
这种方法有个明显的缺点,就是你需要把计算公式 19.9-10
转为 (19.9*10 - 10*10)/10
的这个过程,其实并不灵活,基本上是手工的,如果这个公式是保存在后端数据库,你会发现马上涉及到数学表达式的解析问题。所以,采用这种方法时,应该是页面上显示格式化时的最后环节,你可以随意的对计算过程进行变换修改。
还有,这种方法也不是万能的,比如:
> (19.99 * 100 - 10 * 100)/100
9.989999999999998
> (19.99 * 1000 - 10 * 1000)/1000
9.99
当换成是两位小数的 19.99
,长长有尾巴又回来了。变换时,需要先乘以 1000
,但除以 1000
才能消除精度误差,由此,这种方法很难成为通用的解决方案。
四舍五入 #
js 中提供了原生数学计算库,其中有截位有关的:
上面两个函数有区别,但于我们截位来说,基本够用,并且它们有一个共同点,函数返回的结果都是字符串。
> '19.9' - 10
9.899999999999999
> (19.9 - 10).toFixed(2)
"9.90"
> (19.9 - 10).toFixed(9)
"9.900000000"
> (19.9 - 10).toFixed(5)
"9.90000"
> (19.9 - 10).toFixed(12)
"9.900000000000"
> (19.9-10).toPrecision(12)
"9.90000000000"
计算结果算是得到,但是格式并不是我们所希望的,比如在页面上显示某个商品价格,只会是 9.9
,而不会是 9.90
,更不会是 9.9000000
。使用 toFixed
或者 toPrecision
得到的结果可以用来参与二次计算,字符串数字会被自己解释为数字。如果需要在页面上展现,最好使用 parseFloat
,这样得到的结果就是最符合展现使用的。
> parseFloat((19.9-10).toFixed(12))
9.9
提示,toFixed
和 toPrecision
与上面的换算成整数一个,都应该只用在计算最后展示的环节中,虽然得到的截位后的结果是正确的,但如果是 1.23456
这个5位小数,使用 1.23456.toFixed(2)
之后再去参与计算,将会引起其它方面的误差。
以上的方法都不好使 #
一般来说,像 mathjs.org 这种非原生数学计算库,是使用 javascript 实现的数字计算器,正确性方面不用怀疑。但可能就是它是非原生实现,所以计算性能达不到要求。如果上面所介绍的方法都试过了,还是无法完美解决问题,那么可能要考虑更复杂的方案,把数值计算全部移出 JavaScript,在后端使用 java、python、go 等来计算,把结果反馈给前端页面,如果是带有高小数位的,最好把结果转换成字符串类型,避免在 js 中的浮点转换引起精度误差。