# 0.1 + 0.2 !== 0.3

0.1 + 0.2 == 0.3 乍一看没问题,那么计算机会认为是正确的吗? 0.1 + 0.2 = 0.3 吗?

# 数字

在 js 中整数和小数统称为浮点数,而不像 java 中区分整数和小数。 js ,采用存储浮点数采用的是 IEEE 754 双精度浮点数来存储数值。

# IEEE 754 64位浮点数

  • 第 1 位: 符号位, 0表示正数,1表示负数。
  • 第 2 ~ 12 位,共11位,指数部分。
  • 第 13 ~ 64 位,共52位,小数部分。

越高的位置对数值的影响越大,所表示的精度就越不准确。

# 隐含位置

小数部分,用科学计数法表示之后,默认都是 1.xxx...xxxx 这种形式,所以小数部分会默认把 1 省略掉,不会保存在小数部分。

# 指数部分。

指数部分,最大值 2^11 (2047) , 它是一个无符号整数,范围为 [0, 2047], 但是科学计数法表示数据时是可以为负的,因此约定一个中间数 1023 表示为 0, 因此 [1, 1022] 表示负数, [1024, 2046] 为正。 0和2047被用作特殊数。

# 表示 0.1

0.1 的二进制表示为 0.0 0011 0011....(1100循环)
转成科学计数法为1.100110011001100 * 2^-4
指数部分 = -4+1023 = 1019

0.1 双浮点数存储结构:
    S = 0 满足条件
    E = 1019 不满足条件,需要转为 11 位的二进制   01111111011
    M = 1001100110011循环0011 不满足条件,需要转为 52 位的二进制
    要求 M52 位,所以需要 01M = 1001100110011001100110011001100110011001100110011010 //共 52 位
    拼接 SEM 得到 64 位双精度浮点数
    0011111110111001100110011001100110011001100110011001100110011010

从上面转换后的64位浮点数,我们可以将它转回去,来验证是否是正确的 0.1V = (-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
  = 1.60000000000000008881784197001E0 * 0.625
  = 0.100000000000000005551115123126

我们可以看出来,0.1 在存储的时候是因为无限循环,已经做了一次舍去,导致精度已经不在准确,当我们将存储在内存中的 0.1 转会来,它已经就不再是 0.1 本身了。
那么为什么我们使用 0.1 的时候,它还是 0.1 呢?
在 js 中,是把小数当作整数来看。
在 js 中能够正确显示精度的数为 2^53 (小数部分 52+ 隐含的 1)。
所以最大能够表示的数字就是 2^53 = 9007199254740992. 
对应科学计数尾数是 9.007199254740992 * 2^15, 它的长度是 16 位有效数字。
并且,js 在显示的时候会取到 16 位的精度来表示。

0.10000000000000000555.toPrecision(16)
返回 0.1000000000000000,去掉末尾的零后正好为 0.1

所以我们看到的是 0.1 ,但是它已经不是 0.1 了。

# 表示 0.2

0.2 的二进制为:0.0011 0011 0011(0011循环)
(-1)^0 * 2^(-3) * (1. 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)

0.2 双浮点数存储结构:
    S = 0
    E = 1020,二进制为 01111111100
    M = 1001100110011001100110011001100110011001100110011010
    SEM = 0011111111001001100110011001100110011001100110011001100110011010

和上面一样,保存16位精度, 得到了 0.2

# 0.1 + 0.2

上面即使过程中 0.1 不再是 0.1 ,0.2 不再是 0.2 ,那么为什么 0.1 + 0.2 !== 0.3。

0.10.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

转化为 Double,即 SEM
    (-1)^0 * 2^(-2) * (1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 )
    S = 0
    E = 1021,二进制为 01111111101
    最后的 10 被舍掉,并且进位
    M = 0011001100110011001100110011001100110011001100110100 
    SEM = 0011111111010011001100110011001100110011001100110011001100110100

最终得到了 0.300000000000000004
注意:这里取得是 17 位,而不是16位,是IEEE754的规范中的计算结果。

# 总结

我们可以发现,0.1 在存储的时候产生了误差,0.2 在存储的时候产生了误差,0.1 + 0.2 二进制相加的时候也会产生误差。这一切,就会导致 0.1 + 0.2 !== 0.3