跳到主要内容
  1. Skills/
  2. 前端编程/

js 浮点计算陷阱(float pitfall)

··字数 1558·4 分钟
howto js

很多刚学 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位浮点数(双精度)来表达,所以就有了浮点计算的精度问题。

通常,解决这个问题,有这几种思路:

  1. 使用js 计算库,这些库使用 js 重新实现了数字类型的数值计算,相当于重新实现了一个计算器
  2. 使用整数
  3. 四舍五入,包括尾0、尾9截位
  4. 使用字符串、对结果进行格式化
  5. 不在 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

提示,toFixedtoPrecision 与上面的换算成整数一个,都应该只用在计算最后展示的环节中,虽然得到的截位后的结果是正确的,但如果是 1.23456 这个5位小数,使用 1.23456.toFixed(2) 之后再去参与计算,将会引起其它方面的误差。

以上的方法都不好使 #

一般来说,像 mathjs.org 这种非原生数学计算库,是使用 javascript 实现的数字计算器,正确性方面不用怀疑。但可能就是它是非原生实现,所以计算性能达不到要求。如果上面所介绍的方法都试过了,还是无法完美解决问题,那么可能要考虑更复杂的方案,把数值计算全部移出 JavaScript,在后端使用 java、python、go 等来计算,把结果反馈给前端页面,如果是带有高小数位的,最好把结果转换成字符串类型,避免在 js 中的浮点转换引起精度误差。