Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[js] 第23天 0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么? #80

Open
haizhilin2013 opened this issue May 8, 2019 · 11 comments
Labels
js JavaScript

Comments

@haizhilin2013
Copy link
Collaborator

第23天 0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么?

@haizhilin2013 haizhilin2013 added the js JavaScript label May 8, 2019
@hbl045
Copy link

hbl045 commented May 9, 2019

JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。

@AnsonZnl
Copy link
Contributor

AnsonZnl commented May 9, 2019

用一句话概括就是:

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

@blueRoach
Copy link

0.30000000000000004
0.4
0.020000000000000004

EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位导致计算出现精度丢失问题!

一般使用(0.1 + 0.2 - 0.3 )< Number.EPSILON来解决

@CoderLeiShuo
Copy link

CoderLeiShuo commented Jul 31, 2020

先说结果:

image-20200731152004358

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

说的再简单些:

我们可以把计算机转换二进制存储的过程类比成下面的问题:

image-20200731152004358

当我们把1除以3得到的0.333结果再进行相加,永远加不到1

JavaScript存储数字的标准

JavaScript采用了IEEE754标准来规定数字。在IEEE754标准中,又分为以下几种标准:

  • 单精度
  • 双精度(64位)
  • 延伸单精度
  • 延伸双精度

Javascript中采用的是双精度标准来表示数字,64位的意思就是由0或者1组成这64位,从而表示出一个二进制的数字。

在这64位数字中,并不是01随意地排列组合,IEEE754标准把这64位分成了三个部分

image-20200731122803946

可能现在还不能理解为何要这样划分,接着往下看。

计算机如何存储数字

在研究计算机是如何存储数字前,我们先来回顾一下科学计数法:

image-20200731123300874

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

计算机也采用这种方式来存储数字,不过存储的是二进制的。例如:

image-20200731143025293

进制转换

toString()方法实现进行转换

这里由于篇幅限制,不具体讲解进制转换的问题。

既然我们说在计算机中是通过二进制来表示数字的,我们先把0.10.2转换成二进制来看一下。如何在JavaScript中进行进制的转换呢?

答案是:toString()方法

不要只认为toString()方法是将一个值转换成字符串的,通过向该方法中传入基数参数,toString()可以输出以二进制八进制十六进制,乃至其他任意有效进制格式表示的字符串值

image-20200731121235195

手动将十进制小数转换为二进制

直接用toString()方法得到出的好像并不是一个无限循环的二进制数,那为什么图中标明了‘0110’循环呢?我们手动计算一下,应该就知道了。

十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。

具体做法是:

  • 用2乘十进制小数,可以得到积,将积的整数部分取出;
  • 再用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.10.2相加以后再转换成十进制就已经不再是纯正的0.3了。

image-20200731154413062

解决方法

image-20200731114240040

幸运的是0.10.2得出的这个近似0.3的数不后面很多个0以后才出现4这个数字,因此有多种方法,可以将结果“修正”为正确答案

toFixed()方法

我们可以使用toFixed()方法将相加的结果保留指定位置的小数,例如,这里保留了5位小数。toFixed()方法的结果是一个字符串,可以利用parseInt()或者parseFloat()方法将字符串转换为数值。这里由于最终结果应该是一个小数,因此使用parseFloat()方法。

image-20200731155049649
个人能力有限,如有错误,敬请指正!
补充两篇文章:
揭秘 0.1 + 0.2 != 0.3
为什么「0.1+0.2!=0.3」,而「0.1+0.3==0.4

@MrZ2019
Copy link

MrZ2019 commented Sep 23, 2020

JS中采用的IEEE 754的双精度标准,计算机内部存储数据的编码的时候,导致精度变化。不是所有浮点数都有舍入误差。二进制能精确地表示位数有限且分母是2的倍数的小数(这种情况的小数像乘法0.1*0.2的出五十分之一就不能精确)。

@yangyi1987
Copy link

(0.2 * 1e20 + 0.3 * 1e20)/1e20

@amikly
Copy link

amikly commented Nov 10, 2021

0.30000000000000004
0.4
0.020000000000000004
EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位,这导致计算出现精度丢失问题。
计算机内部存储数据的编码的时候,0.1在计算机内部根本就不是精确的0.1,而是一个有舍入误差的0.1。当代码被编译或解释后,0.1已经被四舍五入成一个与之很接近的计算机内部数字。其它小数同理.

@xiaoqiangz
Copy link

mark js采用的双精度标准,遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位,这导致计算出现精度丢失问题。

@CoderLeiShuo
Copy link

CoderLeiShuo commented May 28, 2022 via email

@an31742
Copy link

an31742 commented 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
Copy link

CoderLeiShuo commented Aug 31, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js JavaScript
Projects
None yet
Development

No branches or pull requests

11 participants