第23天 0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么?
Activity
hbl045 commentedon May 9, 2019
JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。
AnsonZnl commentedon May 9, 2019
用一句话概括就是:
这个问题也算是经常遇到的面试题之一了,楼上说的对,简单来说就是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
blueRoach commentedon Jun 16, 2020
0.30000000000000004
0.4
0.020000000000000004
EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位导致计算出现精度丢失问题!
一般使用(0.1 + 0.2 - 0.3 )< Number.EPSILON来解决
CoderLeiShuo commentedon Jul 31, 2020
先说结果:
之所以会出现
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
smile-2008 commentedon Sep 23, 2020
JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。
yangyi1987 commentedon Nov 3, 2020
(0.2 * 1e20 + 0.3 * 1e20)/1e20
amikly commentedon Nov 10, 2021
0.30000000000000004
0.4
0.020000000000000004
EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位,这导致计算出现精度丢失问题。
计算机内部存储数据的编码的时候,0.1在计算机内部根本就不是精确的0.1,而是一个有舍入误差的0.1。当代码被编译或解释后,0.1已经被四舍五入成一个与之很接近的计算机内部数字。其它小数同理.
xiaoqiangz commentedon May 28, 2022
mark js采用的双精度标准,遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位,这导致计算出现精度丢失问题。
CoderLeiShuo commentedon May 28, 2022
an31742 commentedon Aug 31, 2023
除了含含糊糊的精度损失,你能给出更有营养的解释么?让我们看看到底是为什么!
首先,让我们举一个整数的例子,比如:
十进制「13」:1*(10�^1) + 3(10^0) = 10 + 3 = 13
二进制「1101」:1*(2^3) + 1*(2^2) + 0*(2^1) + 1*(2^0) = 8 + 4 + 0 + 1 = 13
接着,让我们再举一个小数的例子,比如:
十进制「0.625」:6*(10^-1) + 2*(10^-2) + 5*(10^-3) = 0.625
二进制「0.101」:1*(2^-1) + 0*(2^-2) + 1*(2^-3) = 5/8 = 0.625
最重要的一点是你要明白计算机是如何表示小数的:比如二进制的「0.1111111」,无非就是十进制的「1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128」,不过细心的你可能已经发现问题了,计算机这种处理小数的方式存在精度损失的,比如一个十进制的「0.1」,换算成分数的话就是十进制的「1/10」,对比前面的结果,你会发现计算机没办法精确表示它,只能近似等于二进制的「0.00011」,也就是十进制的「1/16 + 1/32 = 3/32」,当然二进制小数点后可以多取几位,可惜结果是只能无限趋近,但永远不可能等于。
下面看看为什么「0.1 + 0.2 != 0.3」,而「0.1 + 0.3 == 0.4」。既然存在精度损失,那么「0.1 + 0.2 != 0.3」也说得过去,我们推算一下为什么「0.1 + 0.3 == 0.4」:
十进制的「0.1」近似等于二进制「0.00011」
十进制的「0.3」近似等于二进制「0.01001」
十进制的「0.4」近似等于二进制「0.01100」
于是,十进制的「0.1 + 0.3」也就是二进制的「0.00011 + 0.01001」:
链接 https://cloud.tencent.com/developer/article/1918183
CoderLeiShuo commentedon Aug 31, 2023
panpanxuebing commentedon Dec 10, 2024
js 采用 IEEE 双精度64位浮点数来保存数字,由于长度有限,所以对于数据的舍入有误差。
而数字的计算也是在转为二进制的,计算结果也会有误差。所以结果看起来正确,其实只是碰巧而已
。
CoderLeiShuo commentedon Dec 10, 2024