笔者曾经面试被问到过 0.1+0.2 的结果是啥,初看这题你可能心中会想,难道不是0.3吗?但你肯定会觉得没那么简单,那今天我们就来探一探究竟。
1. JavaScript中数字的存储机制
在JavaScript中数字是以 IEEE 754 双精度64位浮点数(需VPN)来存储的,它的表示格式为:
这里你可能注意到了,m是52位,能表示的最大值是2 ^ 52 - 1,可这里为什么是53呢?这就涉及到了隐藏位,比如1 * 1.00111100 * 2 ^ -3中,m表示的是1.00111100中的小数部分“00111100”,整数部分1就是隐藏位,这也就是说,只要指数不全为0,那么它的隐藏位就是1。这里顺便提一下,js里整数可以被精确表示的范围是-2 ^ 53 + 1 ~ 2 ^ 53 - 1。
2. 数字表示
现在我们回到这道题目来,我们知道在计算机中,数字都是以二进制存储的,所以我们要先将0.1和0.2转化成二进制,对于十进制转二进制,整数部分除二取余,倒序排列,小数部分乘二取整,顺序排列,所以
0.1 转化为二进制
0.0 0011 0011 0011 0011 0011 0011 … (0011循环)
0.2 转化为二进制
0.0011 0011 0011 0011 0011 0011 0011 … (0011循环)
然后我们用之前说过的 IEEE 754 双精度64位浮点数(需VPN)来表示:
// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
当然,这里的m指的是小数点后的52位,而小数点前的整数部分1就是前面说过的隐藏位。
然后我们把它相加,这里有一个问题,就是指数不一致时,应该怎么处理,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出。
e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m = 0.1100110011001100110011001100110011001100110011001101 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m = 10.0110011001100110011001100110011001100110011001100111 (52位)
e = -2; m = 1.00110011001100110011001100110011001100110011001100111 (53位)
我们看到已经溢出来了(超过了52位),那么这个时候我们就要做四舍五入了,那怎么舍入才能与原来的数最接近呢?比如1.101要保留2位小数,那么结果有可能是1.10和1.11,这个时候两个都是一样近,我们取哪一个呢?规则是保留偶数的那一个,在这里就是保留1.10。
回到我们之前的就是取m=1.0011001100110011001100110011001100110011001100110100 (52位)
然后我们得到最终的二进制数
1.0011001100110011001100110011001100110011001100110100 * 2 ^ -2
=0.010011001100110011001100110011001100110011001100110100
现在转化为十进制,二进制小数转化为十进制的方法是小数点后第一位*2 ^ -1,第二位*2 ^ -2,以此类推,最终我们用等比数列的求和公式得到十进制数为0.30000000000000004,所以0.1 + 0.2的最终结果是
0.30000000000000004
这就是整个推理过程,希望你也能试着推理一下,虽说这属于JavaScript里面比较偏的知识,但对于了解计算机的存储机制还是有好处的。
参考