JS中的运算问题

3 篇文章 0 订阅

目录

问题产生

    1、浮点数的二进制存储

    2、对阶运算

    3、IEEE754标准下的舍入规则

  总结 


问题产生

    JS 中进行浮点数运算时,会有 0.1 + 0.2 = 0.1+0.2=0.300000000000000004 的结果,但同样是浮点数运算,0.3 + 0.4 = 0.7却可以很精确的得到正确结果,出现这样的问题归结于 JavaScript 遵循IEEE754 标准,对浮点数的二进制存储及运算的舍入规则造成的

这个问题点搬运来自知乎的一篇文章:为什么 JavaScript 中 0.1+0.2 不等于 0.3 ? - 知乎 (zhihu.com)

    1、浮点数的二进制存储

      JavaScript遵循IEEE754标准,在64位中存储一个数据的有效数字形式。

       

    ①.第0位为符号位,0表示正数、1表示负数

    ②.第1到11位存储指数部分

    ③.第12到63位存小数部分(尾数部分,即有效数字),由于二进制的有效数字总是表示为 1.xxx...的形式,尾数部分在规约形式下的第一位默认为1,故存储时第一位省略不写,尾数部分f存储有效数字小数点后的xxx..., 最长52位。因此 --> JavaScript提供的有效数字最长为53个二进制位(尾数部分52位 + 被省略的1位)

      0.1、0.2、0.3、0.4、0.7 二进制形式如下:

0.1->0.0001100110011...(0011无限循环)->0-01111111011-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.2->0.001100110011...(0011无限循环)->0-01111111100-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.3->0.01001100110011...(0011无限循环)->0-01111111101-(1 .)0011001100110011001100110011001100110011001100110011(舍)
0.4->0.01100110011...(0011无限循环)->0-01111111101-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.7->0.101100110011...(0011无限循环)->0-01111111110-(1 .)0110011001100110011001100110011001100110011001100110(舍)

      对于52位之后进行舍入运算,此时可看作 "0舍1入"(具体舍入规则在第三部分内容说明),可以知道的是,由于舍入运算,必然存在精度损失的可能

    2、对阶运算

      由于指数位数不同,运算时需要进行对阶运算。对阶过程略,0.1 + 0.2 与 0.3 + 0.4 的尾数求和结果分别如下:

0.1+0.2->10.0110011001100110011001100110011001100110011001100111
0.3+0.4->10.1100110011001100110011001100110011001100110011001101

    求和结果需规格化(有效数字表示),右规导致低位丢失,此时需对丢失的低位进行舍入操作:

0.1+0.2->1.00110011001100110011001100110011001100110011001100111->1.0011001100110011001100110011001100110011001100110100(入)
0.3+0.4->1.01100110011001100110011001100110011001100110011001101->1.0110011001100110011001100110011001100110011001100110(舍)

    即:

      00111 -> 0100

      01101 -> 0110

      此处同样有精度损失。在这里我们可以发现,0.3+0.4对阶阶运算且规格化后的运算结果与0.7在二进制中的存储尾数相同(可对照尾数后几位),而0.1+0.2的运算结果与0.3的存储尾数不同,且0.1+0.2转化为十进制时结果为0.300000000000000004。
此时,虽然0.1+0.2与0.3+0.4进行舍入操作的近似位都为1,但一入一舍导致计算结果与“标准答案”的异同 

    3、IEEE754标准下的舍入规则

      维基百科对最近偶数舍入原则的解释如下:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式),即会将结果舍入为最接近(精度损失最小)且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。

    首先要注意的是,保留小数不是只看后面一位或者两位,而是看保留位后面的所有位。

       如图,可以看到近似需要看三位,保留位(近似后的最低位)、近似位(保留位的后一位)、粘滞位(sticky bit 近似位后的所有位进行或运算后看作一位)。
当粘滞位为1时,舍入规则可以看作0舍1入,近似位为0舍,近似位为1入(即第一部分小数二进制存储为52位尾数时所进行的舍入操作)。
当粘滞位为0时,若近似位为0则舍去。
当粘滞位为0时,若近似位为1,无论舍入精度损失都相同,故需取舍入两种结果中的偶数:保留位为1时入,保留位为0时舍(即第二部分对阶运算规格化时的舍入操作)。

  总结 

    由于IEEE754标准,这样的“bug”不止在JavaScript中会出现,在所有采用该标准的语言中都会存在,实际编程中可以通过设置精度保留位数等方式解决

    开发中若是有两个浮点数的运算判断,则会造成不小的问题

console.log(0.1+0.2 === 0.3)  //false

    对于这个精度误差,这里再提供两种方法进行解决

      1、ES6中,已经为我们提供了一个属性:Number.EPSILON,这个值非常非常小,在底层计算机已经帮我们运算好,并且无限接近0(但不等于0),原理就是只要判断(0.1+0.2)- 0.3 < Number.EPSILON,这个误差的范围内就可以判定 0.1 + 0.2 === 0.3 为 true

//写一个专门判断运算结果是否等价的方法
function numbersEqual(a,b){
  return Math.abs(a-b)<Number.EPSILON;
}

//测试
let a = 0.1+0.2;
let b = 0.3

console.log(numbersEqual(a,b));    //true

      2、还有种办法就是规避掉这类小数计算时的精度问题即可 --> 将浮点数转化成整数计算。因为整数都是可以精确表示的

      通常解决办法就是把计算数字提升 10 的 N 次方倍,再除以 10 的 N 次方。如下:

(0.1*1000 + 0.2*1000)/1000 == 0.3   //true

      这样就避免了 两个浮点数 0.1 和 0.2 直接计算而造成精度误差了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值