第23天 0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么?
JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。
用一句话概括就是:
EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位导致计算出现精度丢失问题!
这个问题也算是经常遇到的面试题之一了,楼上说的对,简单来说就是js中采用IEEE754的双精度标准,因为精度不足导致的问题,比如二进制表示0.1时这这样表示1001100110011...(0011无线循环),那么这些循环的数字被js裁剪后,就会出现精度丢失的问题,也就造成了0.1不再是0.1 了,而是变成了 0.100000000000000002
我们可以来测试一下:
0.100000000000000002 === 0.1//true
那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002:
0.200000000000000002 === 0.2 // true
由此我们可以得出:
0.1 + 0.2 === 0.30000000000000004//true
所以自然0.1+0.2!=0.3。
那么如何解决这个问题;使用原生最简单的方法:
parseFloat((0.1+0.2).toFixed(10)) === 0.3//true
参考:
深度剖析0.1 +0.2===0.30000000000000004的原因:https://www.jianshu.com/p/d6b81e4e25e3
0.30000000000000004
0.4
0.020000000000000004
EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位导致计算出现精度丢失问题!
一般使用(0.1 + 0.2 - 0.3 )< Number.EPSILON来解决
先说结果:

之所以会出现0.1 + 0.2 != 0.3这种问题,原因在于我们现实世界中使用十进制来表示数字,但是计算机中只能使用二进制来表示数字,小数也是用二进制来表示。JavaScript存储二进制数据也是有限度的,正如在现实中我们无法写下一个无限循环的小数一样,只能写个近似数。
说的再简单些:
我们可以把计算机转换二进制存储的过程类比成下面的问题:

当我们把1除以3得到的0.333结果再进行相加,永远加不到1。
JavaScript存储数字的标准JavaScript采用了IEEE754标准来规定数字。在IEEE754标准中,又分为以下几种标准:
Javascript中采用的是双精度标准来表示数字,64位的意思就是由0或者1组成这64位,从而表示出一个二进制的数字。
在这64位数字中,并不是0和1随意地排列组合,IEEE754标准把这64位分成了三个部分

可能现在还不能理解为何要这样划分,接着往下看。
在研究计算机是如何存储数字前,我们先来回顾一下科学计数法:

对于一个非常大的数字来说,我们可以通过科学计数法来表示:例如666000可以被表示为6.66 x 10^5,这样我们就可以只存储一个有效数字6.66,然后记住它的指数位上的数字5,通过这两个简单的数字来表示一个非常大的数字。
计算机也采用这种方式来存储数字,不过存储的是二进制的。例如:

toString()方法实现进行转换这里由于篇幅限制,不具体讲解进制转换的问题。
既然我们说在计算机中是通过二进制来表示数字的,我们先把0.1和0.2转换成二进制来看一下。如何在JavaScript中进行进制的转换呢?
答案是:toString()方法
不要只认为toString()方法是将一个值转换成字符串的,通过向该方法中传入基数参数,toString()可以输出以二进制、八进制、十六进制,乃至其他任意有效进制格式表示的字符串值。

直接用toString()方法得到出的好像并不是一个无限循环的二进制数,那为什么图中标明了‘0110’循环呢?我们手动计算一下,应该就知道了。
十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。
具体做法是:
十进制0.1
0.1 * 2 = 0.2,整数部分是0
0.2 * 2 = 0.4,整数部分是0
0.4 * 2 = 0.8,整数部分是0
0.8 * 2 = 1.6,整数部分是1
0.6 * 2 = 1.2,整数部分是1
0.2 * 2 = 0.4,整数部分是0
...
十进制0.1→二进制0.000110011→二进制科学记数法:1.10011 * 2-4
从上面的计算过程,可以发现,整数部分从0.4那里开始循环,得到的值永远都是一个小数,结果是一个无限循环的数。因此,只能取一个近似数来表示。(这样的话,就会存在精度丢失了)
十进制0.2
0.2 * 2 = 0.4,整数部分是0
0.4 * 2 = 0.8,整数部分是0
0.8 * 2 = 1.6,整数部分是1
0.6 * 2 = 1.2,整数部分是1
0.2 * 2 = 0.4,整数部分是0
0.4 * 2 = 0.8,整数部分是0
0.8 * 2 = 1.6,整数部分是1
0.6 * 2 = 1.2,整数部分是1
0.2 * 2 = 0.4,整数部分是0
...
十进制0.2→二进制0.001100110→二进制科学记数法:1.10011 * 2-3
计算十进制小数0.2也是如此,会发现无限循环,因此只能取一个近似数来代替(同样会发生精度丢失)
因为这两个十进制的小数转换成二进制的小数后,是一个无限循环的数,因此用IEEE754标准来表示数字的话肯定会出现后续的位置无法存储的问题。
因此指数位只有11位,有效数只有52位。有效数部分只能存储52个数字,这样就迫使计算机取一个近似的数字。那么0.1和0.2相加以后再转换成十进制就已经不再是纯正的0.3了。


幸运的是0.1加0.2得出的这个近似0.3的数不后面很多个0以后才出现4这个数字,因此有多种方法,可以将结果“修正”为正确答案
toFixed()方法我们可以使用toFixed()方法将相加的结果保留指定位置的小数,例如,这里保留了5位小数。toFixed()方法的结果是一个字符串,可以利用parseInt()或者parseFloat()方法将字符串转换为数值。这里由于最终结果应该是一个小数,因此使用parseFloat()方法。

个人能力有限,如有错误,敬请指正!
补充两篇文章:
揭秘 0.1 + 0.2 != 0.3
为什么「0.1+0.2!=0.3」,而「0.1+0.3==0.4
JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。
(0.2 * 1e20 + 0.3 * 1e20)/1e20
Most helpful comment
用一句话概括就是:
这个问题也算是经常遇到的面试题之一了,楼上说的对,简单来说就是js中采用IEEE754的双精度标准,因为精度不足导致的问题,比如二进制表示0.1时这这样表示
1001100110011...(0011无线循环),那么这些循环的数字被js裁剪后,就会出现精度丢失的问题,也就造成了0.1不再是0.1 了,而是变成了0.100000000000000002我们可以来测试一下:
那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了
0.200000000000000002:由此我们可以得出:
所以自然
0.1+0.2!=0.3。那么如何解决这个问题;使用原生最简单的方法:
参考:
深度剖析0.1 +0.2===0.30000000000000004的原因:https://www.jianshu.com/p/d6b81e4e25e3