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

Created on 8 May 2019  ·  6Comments  ·  Source: haizlin/fe-interview

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

js

Most helpful comment

用一句话概括就是:

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

All 6 comments

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来解决

先说结果:

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

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

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

Was this page helpful?
0 / 5 - 0 ratings