Background

有个问题可能曾经、甚至现在仍旧让一部分人十分困惑。

我们用 JS 执行一个极其的简单的运算,如:0.1 + 0.2,将会得到一个非常意想不到的结果:0.30000000000000004

虽然结果相差无几,但是作为技术人员,这绝对不可以忽略,理由如下:

  • 万一和 Money 有关呢?
  • 万一你的代码里有类似 if (0.1 + 0.2 == 0.15 + 0.15) 的逻辑呢?结果会如你预期么
  • 或者,万一你就是个处女座呢?

可能大部分人都会在一些论坛上寻求帮助,并且得到有很多方法可以帮助其解决问题。

但是这里我会尝试分析一下其中的根本原因以及解决办法。

Why

简单一句话概括解释为什么你会得到意想不到的结果:

因为在计算机内部,使用的二进制浮点根本就不能准确地表示像 0.1, 0.2 或 0.3 这样的数字。

当编码或解释代码时,你的 “0.1” 其实已经舍入为和该数字的最接近的数字,即使在计算发生之前已经会导致小的舍入误差(是的,所有语言都是这样,这锅 JS 不背)。

WTF? 计算机如此愚蠢?

这不是愚蠢,只是不一样。 10 进制数字不能准确地代表像 1/3 这样的数字,所以你必须用 0.3333… 跟一大堆循环,当然你也不会指望 0.33333… * 3 等于 1。

Now, 所有 CSer 都在大一学过的计算机导论派上用场了。 =,=

为什么要用二进制?
计算机使用二进制数是因为它们在处理这些数据时速度更快。

为什么二进制更快?
计算机的固件最最底层是基于数十亿的电气元件,只有两个状态(通常是低电压和高电压)。 通过我们将其解释为 0 (低)和 1(高)。基于这样的底层,构建用于存储二进制数的电路以及对其进行计算都非常高效和容易。
当然,也可以使用二进制电路模拟十进制数的行为,但效率较低。 如果计算机在内部使用十进制数,那么它们在同等硬件水平上的内存将会更少,而且速度更慢。
二进制数和十进制数之间的差异对于大多数应用程序来说并不重要。
因此逻辑选择是基于二进制数构建计算机,并且实际上需要对需要十进制的应用程序进行一些额外的处理。

你说的都有理,可是为什么二进制表示不了 ‘0.1’、 ‘0.2’、 ‘0.3’?
如果你问到这个问题,我只想摸摸你的头。。。举个栗子,十进制的 14,分别用十进制和二进制表示如下:

Type Value Value (InDec)
Decimal \(={1}\times{10^1} + {4}\times{10^0}\) 14 14
Binary \(={1}\times{2^3} + {1}\times{2^2} + {1}\times{2^1}+ {0}\times{2^0}\) 1110 14

再来一个,小数 0.625

Type Value Value (InDec)
Decimal \(={6}\times{10^{-1}} + {2}\times{10^{-2}} + {5}\times{10^{-3}}\) 0.625 0.625
Binary \(={1}\times{2^{-1}} + {0}\times{2^{-2}} + {1}\times{2^{-3}}\) 0.101 0.625

So far, so good.

那你尝试一个十进制小数 0.1 试一试?

Type Value Value (InDec)
Decimal \(={1}\times{10^{-1}}\) 0.1 0.1
Binary \(={0}\times{2^{-1}} + {0}\times{2^{-2}} + {0}\times{2^{-3}} + {1}\times{2^{-4}} + {1}\times{2^{-5}}\) 0.00011 0.09375

0.00011 已经到小数点后 5 位了,还是凑不齐 0.1 对不对? 实际上从后四位 0011 就开始循环了。结果是只能无限趋近于 0.1 无法真正等于 0.1。

在 JS 中应该是做了一定位数后的 “入” 处理。这时候 0.1 + 0.3 = 0.30000000000000004 是不是能理解了?

然而,许多语言在处理的时候,在一定误差范围内(通常极小)会将结果处理为正确的目标数字,而不是像 JS 一样将存在误差的真实结果转换成最接近的小数输出。

How

如何处理这个问题。

  • 如果你真的需要在你的代码中做浮点数的四则运算或者大小判断,特别是当你处理钱时,可以使用一个特殊的 decimal type 如 BigDecimal
  • 如果您不想看到所有这些额外的小数位数:在显示时,也可以用toFixed()toPrecision() 之类将结果格式化为固定的小数位数。
  • 如果没有可用的十进制数据类型,另一种方法是使用整数,例如。 完全以整数最小位数为最小单位计算。

Q&A

  • 0.1+0.4 的结果是 0.5 ,没有问题,是不是啪啪啪打脸了?怎么解释?
    这种情况,首先 0.5 本身是可以精确地表示为二进制浮点数的 0.1;其次输入的两个数字中的舍入误差可能相互抵消,但不一定会抵消。


  • 如果您有兴趣,请在 issue 中提供关于浮点数可能出现的其他问题以及更多解决方案的深入解释

如果觉得文章对你有帮助的话,去 Github Repo 给个 star 也是最好不过的 : )