JavaScript 0.1+0.2 != 0.3

因为 JavaScript 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有这个问题。

我们知道,科学计数法中 30000 可以写成 3x104,以 10 为底数 4 为指数的科学计数法。在 IEEE754 标准中它是二进制数,底数为 2。

举个例子,十进制数 150,使用双精度浮点数表示,通过短除法计算,最后一个余数为高位值,于是拿到 150 对应的二进制数位 1001011,也就等于 2^8 * 0.1001011。表示如下:

1
2
// D 表示十进制,B 表示二进制
150D = 2^8 * 0.1001011B // 后面省略了 46 个 0

小数的表示法采用的是乘二取整,与整数不同的是,第一个计算得到的整数位为最高位,故 0.1 对应的二进制数为 0.000110011(0011),也就等于 2^-4 * 0.1100110011(0011)。如 0.1,它的二进制表示为:

1
2
// (0011) 表示循环
0.1D = 2^-4 * 0.110011(0011)

如果一个数既包含整数部分,又包含小数部分,其表示法的计算,需要分拆为整数和小数两部分,然后相加得到结果。

根据上面算出这些值:

1
2
3
0.1D = 2^-4 * 1.1001100110011001100110011001100110011001100110011010B
0.2D = 2^-3 * 1.1001100110011001100110011001100110011001100110011010B
0.3D = 2^-2 * 1.0011001100110011001100110011001100110011001100110011B

0.1 + 0.2 时,先将两者指数统一为 -3,故 0.1 小数点向左移一位,于是:

1
2
3
4
   0.1100110011001100110011001100110011001100110011001101B
+ 1.1001100110011001100110011001100110011001100110011010B
------------------------------------------------------------
= 10.0110011001100110011001100110011001100110011001100111B

得到的二进制数为:

1
10.0110011001100110011001100110011001100110011001100111B

小数点往左移一位使得整数部分为 1,此时尾数部分为 53 位,进一舍零,于是得到最后的值是:

1
2^-2 * 1.0011001100110011001100110011001100110011001100110100

这个值转化成真值为:0.30000000000000004。IEEE 754 双精度,六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1 和 0.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

那么 0.1 + 0.2 = 0.30000000000000004。

解决办法: parseFloat((0.1 + 0.2).toFixed(10)) 或者 Number.EPSILON