我们都知道0.1 + 0.2 = 0.3
,但是在JavaScript中0.1
加0.2
得到的不是0.3
,而是 0.30000000000000004
。由于这种微小的舍入错误,导致很难测试特定的浮点值。比如下面的例子:
let a = 0.1
let b = 0.2
if (a + b == 0.3) { // false
console.log("You got 0.3.")
}
这里检测两个数值之和是否等于0.3
。如果两个数值分别是0.05
和0.25
,或者0.15
和0.15
,那没问题。但如果是0.1
和0.2
,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。
let a = 0.05
let b = 0.25
if (a + b == 0.3) {
console.log("You got 0.3.") // You got 0.3.
}
之所以存在这种舍入错误,是因为使用了 IEEE 754 数值(64位),即52位小数、11位指数、1位符号,小数超过52位就会发生精度丢失,这种错误并非ECMAScript所独有。其他使用相同格式的语言也有这个问题。
符号位 (Sign): 决定数是正数(s=0)还是负数(s=1)。
指数位 (Exponent): 是 2 的幂(可能是负数),它的作用是对浮点数加权。
有效数字位 (Significand): 是二进制小数,它也被称为尾数位(Mantissa)、系数位(Coefficient)。
在这之前,我们先来了解一个小概念,当我们要标记或运算某个较大或较小且位数较多时会使用科学记数法,这样会少浪费很多空间和时间,例如:19971400000000=1.99714×10^13。
这里用0.125
作为示范,转换为二进制,用的是乘二取整,只计算小数位,从上至下得到0.001
。
0.125 x 2 = 0.25
,整数部分是0
0.25 x 2 = 0.5
,整数部分是0
0.5 x 2 = 1.0
,整数部分是1
接下来,我们把0.1
转换为二进制,对应是1.10011... × 2^-4
。
0.1 x 2 = 0.2
,整数部分是0
0.2 x 2 = 0.4
,整数部分是0
0.4 x 2 = 0.8
,整数部分是0
0.8 x 2 = 1.6
,整数部分是1
0.6 x 2 = 1.2
,整数部分是1
0.2 × 2 = 0.4
,整数部分是0
这里我们发现无法进行整除,循环0110
,那么十进制的0.2
也是会有同样的结果,因为我们可以从上面的结果里面看到从0.2
开始进行乘2
就一样会得到一个无限循环,循环的部分依旧是0110
。那么肯定会出现后续的位置无法存储进去,这样就迫使计算机取一个近似的数字。
我们知道3
个1/3
相加可以得到3/3
也就是1
,不过这是十进制,就类似于我们把1
除以3
得到的0.333
的无限循环以后再进行相加,很明显这些无限循环的0.333
相加怎么也没有办法得到一个完整的1
。
解决方法: 因为小数乘二取整会有无限循环的情况,但是整数除二取余是不会的,所以整数部分不会出现精度丢失问题。
let a = 0.1
let b = 0.2
if ((a * 10 + b * 10) / 10 == 0.3) {
console.log("You got 0.3.") // You got 0.3.
}