js浮点数运算不精确 如何解决_0.1 + 0.2 为什么不等于 0.3--奇怪的相等与不等

现象

0.1 + 0.2 === 0.3
false

9a8b44dda451a38efc5eac09788230ad.png


前置知识

  • 在强类型语言中,整数和小数是区分对待的,整数用整型,小数用浮点型
  • 目前有两种显示浮点数的方法:单精度和双精度,在进行浮点运算时,单精度使用 32 位,而双精度使用 64 位。
  • 本文对浮点型简略带过,关于浮点型不了解的同学,推荐<你不知道的浮点型>

探究
0.1 + 0.2 === 0.3为false为例;
要解释这个问题,必须先解释js中小数存储的原理;事物出现必有其原因,问题驱动:

  1. js中数字类型无区分(整数、小数),那它们转化为二进制后都是怎么存储的呢?

小数有要求(乘R取整,可能出现循环无穷,可见下面的0.1),整数易转化无要求(除R取余),很简单的,要保证小数的运算标准,所以必然是浮点型存储;

  1. js选择的是哪种?

与许多其他编程语言不同,JavaScript 并未定义不同类型的数字数据类型,而是始终遵循国际 IEEE 754 标准,将数字存储为双精度浮点数。
IEEE754 标准的双精度浮点数

042da89d94177cc0d6df5091239cdb02.png


IEEE 754 浮点数由三个域组成,分别为 sign bit (符号位)、exponent bias (指数偏移值) 和 fraction (尾数)。64 位中,sign bit 占 1 位,exponent bias 占 11 位,fraction 占 52 位。

实操
好的,让我们开始计算模拟计算机开始计算0.1+0.2吧,关键是小数转IEEE 754标准,之后的加和很简单
转化过程相当于把大象装冰箱:

  1. 将 0.1 转换为二进制表示
  2. 将转换后的二进制通过科学计数法表示
  3. 将通过科学计数法表示的二进制转换为 IEEE 754 标准表示

将 0.1 转换为二进制表示


我们都知道小数转二进制用乘R取整的方法,运算如下(进制转换及原补反移码不了解的同学,推荐<浅显易懂的原补反移>)

03591b88f3f80722292e680e1e213eb2.png

得到结果0.00011001100110011…(循环0011)


将转换后的二进制通过科学计数法表示


0.00011...(无限重复 0011) 通过科学计数法表示则是 1.10011001...(重复 1001)*2-4
将通过科学计数法表示的二进制转换为 IEEE 754 标准表示
进行规格化,简言之就是求得 exponent bias 和 fraction ,

  • exponent bias (指数偏移值)
    双精度浮点数固定偏移值 (2^(11-1)-1) 加上指数实际值(即 2^-4 中的 -4) 的 11 位二进制表示。为什么是 11 位?因为 exponent bias 在 64 位中占 11 位
因此 0.1 的 exponent bias 等于 1023 + (-4) = 1019 的11 位二进制表示,即 011 1111 1011。

fraction(尾数)

fraction 占 52位所以抽取 52 位小数(多出来的采用四舍五入制)

1001...(中间有 11 个 1001)...1010 (请注意最后四位,是 1010 而不是 1001,因为四舍五入有进位,这个进位就是造成 0.1 + 0.2 不等于 0.3 的原因)。

到此,终于可以将 0.1 转换为 IEEE 754 表示了

   0       011 1111 1011   1001...( 11 x 1001)...1010
(sign bit) (exponent bias)      (fraction)

警报警报,误差出现


此时如果将这个数转换为十进制,可以发现值已经变为 0.100000000000000005551115123126 而不是 0.1 了。
同理将0.2和0.3同过程,亦会有误差。
加和自然不等。


奇怪的相等
在 javascript 语言中,Number 下分别有两个常量 MAX_VALUE 和 MAX_SAFE_INTEGER。
其中,MAX_VALUE 表示在 JavaScript 里所能表示的最大数值,MAX_SAFE_INTEGER 表示在 JavaScript 中最大的安全整数,他们的值分别如下:

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true

前置知识


Number.MAX_SAFE_INTEGER与Number.MAX_VALUE
上面的定义是不是很模糊,最大还能理解,最大安全是指什么?了解思考一番后,我们这样来理解,我们从上面已经得知了js(其实只要遵循 IEEE 754规范的都是)存储数字的规则,问题驱动:


js中最大的数怎么表示?
聪明的你肯定会想到是,从科学计数法的角度而言,自然是尾数最大,指数最大;

  • 尾数即52位全取一就好了。
  • 阶码即11位全取一

很简单对吗?对不起给大家挖坑了,其实不对,这个数在js中属于NAN,为什么呢?龟腚(IEEE754对三种特殊的情况进行了规定)

ce81036cad23aedd85e9b33bfb87035d.png


至于为什么有这个规定。。想了很久没想明白,战略性放弃,回头补上。
这样的话,我们就可以得到最大数了

  • 尾数即52位全取一就好了
  • 阶码即11位除最后一位外全取一

得到的是什么呢?(2^53 – 1) 2^(2046 – 1023) = (2^53 – 1) 2^971
这既是Number.MAX_VALUE的值
控制台打印

(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true

js中安全范围内最大的整数怎么表示?


所谓的安全,就是大于这个数的整数不一定可以精确表示,其实很好理解,只有尾数溢出了52位,才会出现“四舍五入”的情况,再加上默认存在的首位1,这也就意味着,整数只要在 2^53 – 1内,都是绝对安全的,不会出现精度损失问题;


那么,为什么大于这个数,就会出现呢?我们以此数+1为例,也就是这个相等的奇怪情况

const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true

Number.MAX_SAFE_INTEGER 用原码表示是

0 10000110100 1111111111111111111111111111111111111111111111111111

IEEE 754 规格化后

11111111111111111111111111111111111111111111111111111

Number.MAX_SAFE_INTEGER+1 的原码是

100000000000000000000000000000000000000000000000000000

IEEE 754 规格化后

0 10000110101 0000000000000000000000000000000000000000000000000000 0(溢出,小于0.5r,r代表进制,舍去)

Number.MAX_SAFE_INTEGER+1 的原码是

100000000000000000000000000000000000000000000000000001

IEEE 754 规格化后

0 10000110101 0000000000000000000000000000000000000000000000000000 1(溢出,为0.5r,r代表进制,且最后一位为0,不进位舍去)

注意到我们省去掉了一位,按照向偶舍入的规则,还是不会产生进位。这个时候就有问题了,这个数跟刚才那个数竟然是相等的,我们来验证下

控制台打印

const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true

到此,我们就真正原理上解释了js中奇怪的相等与不等现象

参考文章

JS-为什么 0.1 + 0.2 不等于 0.3 ?

浮点数阶码的计算和表示

JS 中的 MAX_VALUE 和 MAX_SAFE_INTEGER 是怎么来的
原文作者:南方小菜
原文链接:https://juejin.im/post/6870045627623931912
原文来源:掘金

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值