踏上0.1+0.2不等于0.3的不归路

参考文章:
http://xinzhi.wenda.so.com/a/1537180588200142
https://juejin.cn/post/6927217000112455687

0.1+0.2=0.3吗?

1. 在得出答案前先要考虑清楚 JS是怎么计算的?

重要的事说三遍: JS内部所有的计算都是通过二进制进行计算的
重要的事说三遍: JS内部所有的计算都是通过二进制进行计算的
重要的事说三遍: JS内部所有的计算都是通过二进制进行计算的

2. 十进制与二进制间的换算规则

权重的概念:数字中某位的权重:2的(该位所在的位数(从右至左)-1)次方

比如:

0的权重为:2^(1-1)=1

1的权重为:2^(2-1)=2

2的权重为:2^(2-1)=2

2.1二进制转化为十进制

方法:把二进制数按权展开,相加即的十进制数。
在这里插入图片描述

2.2十进制转化为二进制

    1. 整数位转化法
      方法:十进制数除2取余法,即十进制数除2,余数为权位上的数,得到的商值继续除2,依次步骤继续向下运算知道商为0为止。
      在这里插入图片描述
    1. 小数位转化法
      方法:乘2取整,顺序排列
      在这里插入图片描述

整数部分与2取余(最后一个余数到第一个余数),小数部分与2取整(从第一个整数到最后一个整数)

0.1+0.2计算过程

了解完1、2两点,开始探究步入0.1+0.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.4 * 2 = 0.8 --------- 整数部分为0
    0.8 * 2 = 1.6 --------- 整数部分为1
    0.6 * 2 = 1.2 --------- 整数部分为1
    由上可知0.1的二进制是:0.0001 1001 1001 1001 …
  • 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的二进制是:0.0011 0011 0011 0011 …
    注意:
    0.1和0.2转成的二进制是无穷的
    浏览器中使用浮点数形式的二进制来存储二进制,因此需要把上面转化的二进制转化成浮点数形式的二进制

转化成浮点数

浮点数分为单精度浮点数(32位操作系统)与双精度浮点数(64位操作系统)。由于现在的操作系统大多是64位操作系统,32位操作系统正不断被淘汰,因此这里只讲解如何将二进制转化成双精度浮点数的二进制。
float 单精度

符号位指数位小数位
se
1bit8bit23bit
00000000000000000000000000000000

double 双精度

符号位指数位小数位
se
1bit11bit52bit
0000000000000000000000000000000000000000000000000000000000000000
  • 符号位:正.数为0,复数为1
  • 指数位:阶数+偏移量,阶数是:2^(e-1),e为阶码的位数,偏移量是把小数点移动到整数位只有1时移动的位数,整数表示向左移,负数表示向右移;
  • 小数位:即二进制小数点后面的数

指数位详解
如果你知道为什么32位浮点数的指数偏移量是127,你就能知道为什么64位浮点数的指数偏移量是1023。

在32位浮点数中,指数位有8位,它能表示的数字是从0到2的8次方,也就是256(0~255)。但是指数有正有负,所以我们需要把256这个数字从中间劈开,一半表示正数,一半表示负数,所以就是-128到+128。哦,不对,忘记了中间还有个0,所以只能表示-128到127这256个数字。那么怎么记录负数呢?一种作法是把高位置1,这样我们只要看到高位是1的就知道是负数了,所谓高位置1就是说把0到255这么多个数字劈成两半,从0到127表示正数,从128到255表示负数。但是这种作法会带来一个问题:当你比较两个数的时候,比如130和30,谁更大呢?机器会觉得130更大,但实际上130是个负数,它应该比30小才对啊。所以为了解决这个麻烦,人们发明了另外一种方法:干脆把所有数字都给它加上128得了,这样-128加上128就变成了0,而127加上128变成了255,这样的话,再比较大小,就不存在负数比正数大的情况了。

但是我要得到原来的数字怎么办呢?这好办,你只要再把指数减去128就得到了原来的数字,不是吗?比如说你读到0,那么减去128,就得到了负指数-128,读到255,减去128,就得到了127。

那为什么指数偏移是127,不是128呢?因为人们为了特殊用处,不允许使用0和255这两个数字表示指数,少了2个数字,自然就只好采用127了。

同理,64位浮点数,指数位有11位之多,2的11次方是2048,劈一半作偏移,可不就是1024吗?同理,去掉0和2048这两个数字,所以就用1023作偏移了。

80位扩展双精度,一个道理。

把0.1转成的二进制0.00011001100110011…转成浮点形式的二进制。
先把小数点移动到整数位只有1,要向右移动4位,故偏移量为-4,通过指位数的计算公式2^(11-1)-1-4=1019,把1019转成二进制为1111111011,不够11为要补零,最终得出指数为01111111011;
小数位为100110011001…,因为小数位只能保留52位,第53位为1故进1。
转化的结果如下所示
0.1
0 01111111011 10011001100110011001100110011001100110011001100110011010

0.2
0 01111111100 10011001100110011001100110011001100110011001100110011010

浮点数相加时,需要先比较指位数是否一致,如果一致则小数位直接相加,如果不一致,要先把指位数调成一致的,指位数小的向大的调整。

十进制0.1的浮点数的指位数是1019,十进制0.2的浮点数的指位数是1020。故要把0.1的指位数加1,即把0.1的小数点向左移动1位,另外浮点数的整数位固定为1,过程如下所示

1.1001100110011001100110011001100110011001100110011010   原先
0.11001100110011001100110011001100110011001100110011010  移动后  
0.1100110011001100110011001100110011001100110011001101   将小数的第53位舍去,因为为0故不需进1

指位数调整一致后开始计算,0.1+0.2小数位得到了53位,截掉小数位的最后一位相当把小数点向左移了一位,故指数位要加1,此时的指数是0.2的指数102 0,加1后变成1021 ,转成二进制为01111111101 ,那么相加后的浮点数如下所示:

在这里插入图片描述

浮点数转成十进制

二进制浮点数计算结束后,把结果(二进制的浮点数)转成十进制,其转换公式为图片
s是符号位为0或1,e为浮点数指数位转成十进制的值,i表示小数位从左到右的位数,第一位 i=1 ,图片表示每一位的值为0或1。

按着公式把二进制的浮点数转成十进制结果如下所示:
0.3000000000000000444089209850062616169452667236328125
由于精度问题,只取到0.30000000000000004。
在这里插入图片描述

答案

  • 0.1+0.2 不等于 0.3 。因为在 0.1+0.2 的计算过程中发生了两次精度丢失。
  • 第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后第53位的数要进行为1则进1为0则舍去的操作,从而造成一次精度丢失。
  • 第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。

扩展

怎么解决 0.1+0.2 不等于 0.3 这个问题”。

  • 方法1:使用toFixed限制小数的位数
  • 方法2:
function accAdd(arg1, arg2) {
	var r1, r2, m;
	try { r1 = arg1.toString().split(".")[1].length } catch (e) { r1 = 0 }
	try { r2 = arg2.toString().split(".")[1].length } catch (e) { r2 = 0 }
	m = Math.pow(10, Math.max(r1, r2))
	return (arg1 * m + arg2 * m) / m
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值