对于0.1 + 0.2 !== 0.3这个问题,查阅了很多资料,都感觉说的并不是十分完美,最近在看一本书《JavaScript悟道》,作者是Douglas CrockFord,其为JSON,JSLint的创造者,他还有另一本很著名的书籍名字叫《JavaScript语言精粹》,从这本书中,我找到了,为什么在JavaScript中 0.1 + 0.2 !== 0.3这问题的答案。下面我从数值类型的底层原理来分析一下这个问题。
浮点数的二进制表示方法
首先来说一下十进制数与二进制数据的转换。
- 整数:对整数位除以2,直到不能除尽为止,所得余数从后向前排列即是这个十进制整数的二进制表示。
- 小数:对小数位乘以2,取整数位值,直到小数位为0为止,整数位取值的排列即是这个十进制小数的二进制表示
浮点数背后的思想很简单——把一个数拆成两部分来存储。第一部分是有效位数(significand)、第二部分被称为指数(eexponent)表示小数点应该插在系数的哪个位置。
JavaScript的浮点数背后的实现就是如此,js的浮点数实现是用的IEEE 754标准的一个子集,在js中浮点数为64位二进制数构成,其中1位符号位(sign)、11位指数位以及53位有效位数。这些位存放好后,他们代表的值就是 sign * significand * (2**exponent) 。
对于绝大多数语言来说,精确表示小数是一件很困难的事情,根据上面的计算规则,对于小数来说,如果想有限位数表示一个精确的小数,则这个十进制小数的值一定是5的倍数。于是对于JavaScript的解决方法便是取其前52位作为该十进制数的表示。
安全运算
我们知道在JavaScript中最小的正数为Number.EPSILON,最大整数位Number.MAX_SAFE_INTEGER,而在安全数之外的数,JavaScript不能安全的表示,例如:
//Number.MAX_SAFE_INTEGER的值为9007199254740991
//在JavaScript中进行如下运算
9007199254740992 + 1 //9007199254740992
可以看到在安全整数外的数据进行运算时其结果变得不再稳定,也就是说,在JavaScript中只有所有的运算因子、运算结果、以及中间的运算过程都是安全整数的情况下,才能进行精确的整数运算,才使用加法结合律和乘法分配律。
举个例子:
//在JavaScript中进行运算
(0.1 + ( 0.2 + 0.3)) //0.6
((0.1 + 0.2) + 0.3) //0.6000000000000001
我们可以看出来,即使是相同的运算,在数据的不安全的情况下,不同运算顺序计算所得的结果也不一样,为什么会出现这样的问题呢?
0.1 + 0.2 !== 0.3
这就要来说一说这个最近很火的内容了。为什么 0.1 + 0.2 !== 0.3?
我们从前面可以知道,十进制小数在转为二进制小数的过程中,大多数小数是无法精确表示的,JavaScript为了表示这些数便取了这些数的前52位,以下为0.1 - 0.9在JavaScript中的二进制表示(末尾的0被省略所有):
{
"0.1": "0.0001100110011001100110011001100110011001100110011001101",
"0.2": "0.0011001100110011001100110011001100110011001100110011010",
"0.3": "0.0100110011001100110011001100110011001100110011001100110",
"0.4": "0.0110011001100110011001100110011001100110011001100110100",
"0.5": "0.1000000000000000000000000000000000000000000000000000000",
"0.6": "0.1001100110011001100110011001100110011001100110011001100",
"0.7": "0.1011001100110011001100110011001100110011001100110011000",
"0.8": "0.1100110011001100110011001100110011001100110011001101000",
"0.9": "0.1110011001100110011001100110011001100110011001100110100"
}
可以看出
0.1在JavaScript中的小数位二进制表示为0001100110011001100110011001100110011001100110011001101
0.2在JavaScript中的小数位二进制表示为0011001100110011001100110011001100110011001100110011010
两者进行二进制相加的二进制数表示为 0100110011001100110011001100110011001100110011001100111
0.3在JavaScript中的小数位二进制表示为0100110011001100110011001100110011001100110011001100110
可以看出在二进制运算过程中,0.1 + 0.2相加之后的结果与0.3在JavaScript中的二进制数表示相比,在第52位的数据两者是不同的,这也就是为什么0.1 + 0.2 !== 0.3的原因了
同理我们就可以知道为什么(0.1 + ( 0.2 + 0.3))与((0.1 + 0.2) + 0.3)仅仅只是运算顺序不同,而结果却竟然不一样的原因了。
下面我们从IEEE 754的标准来分析一下。
底层原理
Douglas CrockFord提供了调试数组类型的函数:
function deconstruct(number) {
let sign = 1;
let siginficand = number;
let exponent = 0;
if(siginficand < 0){
siginficand = -siginficand;
sign = -1;
}
//将系数不断除以2,直到趋近于0为止。
//然后将-1128相加到exponent。-1128就是Number.MIN_VALUE的指数减去有效位数在减去符号位的结果
if (Number.isFinite(number) && number !== 0) {
exponent = -1128
let reduction = siginficand
//Number.MIN_VALUE为最小的安全正数,Number.MIN_VALUE === 0的结果为false,
//但是比Number.MIN_VALUE更小的正数,如Number.MIN_VALUE / 2 === 0 此时结果为true
while(reduction !== 0){
exponent += 1;
reduction /= 2;
}
//当指数为0时可以认为数值是一个整数,如果指数部位0,则通过校正系数来使其为0
reduction = exponent;
while(reduction > 0) {
siginficand /= 2;
reduction -= 1;
}
while(reduction < 0) {
siginficand *= 2;
reduction += 1;
}
}
return {
sign,
siginficand,
exponent,
number
}
}
我们将0.3和0.1 + 0.2放入这个函数中,得到:
deconstruct(0.3)
/*{
"sign": 1,
"siginficand": 10808639105689190,
"exponent": -55,
"number": 0.3
}*/
console.log(deconstruct(0.1 + 0.2))
/*{
"sign": 1,
"siginficand": 10808639105689192,
"exponent": -55,
"number": 0.30000000000000004
}*/
我们使用sign * significand * (2**exponent)进行计算,可以发现:
一个值是0.29999999999999998889776975374843459763683319091796875
另一个是0.3000000000000000444089209850062616169452667236328125两个值都不是0.3。
我们可以发现,就IEEE 754标准来说,实现一个二进制小数是十分困难的。在我们需要用到高精度整数,高精度浮点数的时候,现在也有比较成熟的JavaScript库可以使用,《JavaScript悟道》这本书Douglas CrockFord也为我们提供了如何实现高精度整数,高精度浮点数,以及高精度有理数的函数方法。