当小数遇上二进制——全面解析JS中的小数二进制计算(附赠0.1+0.2 !== 0.3问题解释)

二进制小数如何转换为十进制

二进制转换十进制的方法是:

  • 从二进制数的最低位开始,每一位乘以对应的2的幂数,然后将最终的结果小数部分与整数部分分别相加
  • 对应的2的幂,以个位为0,向高位依次增1,向地位依次减1;

举个例子: 以二进制小数1100.0011为例:

二进制小数位1100.0011
对应2的幂3210.-1-2-3-4
乘幂计算1 * 231 * 220 * 210 * 20.0 * 2-10 * 2-21 * 2-31 * 2-4
结果8400.000.1250.0625

当幂为负数时,乘幂计算实际上就是除以对应幂数的倒数,即1 * 2-3 = 1 / 23 = 1/8 = 0.125;

所以最终的结果就是 8 + 4 . 0.125 + 0.0625 = 12.1875;

我们在浏览器控制台验证一下,看是否正确:

image-20210927105831566

二进制转十进制javascript实现:


// 这里参数n要直接传入二进制小数的字符串形式,
//如果直接传入一个二进制小数,无论是利用'' +n 还是利用String(n)去转字符串,都会将原值截断,只保留19位,导致最后计算错误
function BinToDec(n) { 
    // let arr = ('' + n).split('.');  这样会导致计算错误。
    let arr = n.split('.'); 
    let ZS = arr[0].split('');
    let XS = arr[1] ? arr[1].split(''): [];
    let zsSum = 0;
    let xsSum = 0;
    let i = 0;
    let j = 1;
    while (ZS.length) {
        let num = parseInt(ZS.pop());
        zsSum += Math.pow(2, i) * num;
        i++;
    }
    if(XS.length) {
        while(XS.length) {
            let num = parseInt(XS.shift());
            xsSum += Math.pow(2, -j) * num;
            j++;
        }
    }
    return zsSum + xsSum;
}

十进制小数如何转为二进制小数

十进制小数转换为二进制是整数部分与小数部分分别计算,然后再相加的。

整数十进制转二进制 —— 除2取余法【短除法】

不断除以2,直到商为0,每一步得到的余数依次由低到高填充到二进制的位置里。

因为任何一个十进制除以2的余数要么是1,要么是0,所以最后这些余数就构成了最后的二进制数。

比如将174 这个数字转换为二进制的过程:

除以2余数二进制第几位
174/28700
87/24311
43/22112
21/21013
10/2504
5/2215
2/2106
1/2017

所以,得到十进制174转换为二进制为: 10101110 (注意由高位到低位书写,和表格中计算得到的顺序相反)

javascript 递归实现:

/**
    * [integerToBin 整数转二进制]
    *
    * @param   {[type]}       n    [n description]
    *
    * @return  {[]}                [return description]
    */
   function integerToBin(n) {
       // 如果商大于0,继续递归调用,否则返回空字符串用于与前面的结果连接
        if (n > 0) {
            // 获得数字除2后的商
            let quotient = parseInt( n / 2);
            let remainder = n % 2;
            return integerToBin(quotient) + '' + remainder;
        }
        return '';
   }

小数十进制转二进制——乘2取整法

  1. 将小数的小数部分取出,乘以2,将得到的结果中的整数部分作为二进制小数的项

  2. 得到结果中的小数部分重复第一步的步骤

  3. 直到某一步乘以2的值的小数部分为0,或者小数部分形成循环小数则停止

    比如 0.1875这个十进制小数,计算转换为二进制的过程:

    乘以2整数部分小数部分
    0.1875 * 20.37500.375
    0.375 * 20.7500.75
    0.75 * 21.510.5
    0.5 * 21.010

    得到结果为:0011 (与整数转换不同,这里是顺序)

    javascript 的递归实现:

/**
     * [fracToBin 小数转二进制]
     *
     * @param   {[type]}       n     [n 需要转换为二进制的小数]
     * @param   {[type]}       bits  [bits 准换后的二进制小数的位数]
     * @param   {undefined[]}  bin   [arr 转换后的二进制小数,默认为空]
     *
     * @return  {[]}                 [return description]
     */
     function fracToBin(n, bits = 49, bin = '') {
        // 如果二进制位小于给定的位数
        if (bin.length < bits) {
            // 1. 将需要转换的小数分为整数部分与小数部分,获得小数部分
            let xs =  ('' + n).split('.')[1];
            // 2. 小数部分乘以2
            //  2.1小数部分字符串转数字
            let xsNumber = parseFloat('0.' + xs);
            // 2.2 小数部分乘以2
            let a = xsNumber * 2;
            // 3. 取出数字的整数部分拼接到二进制小数的结果中
            let ta = ('' + a).split('.');
            bin += ta[0];
            let na = parseFloat('0.' + ta[1]);
            // 如果小数部分等于0,则返回
            if (parseFloat(na) == 0) {
                return bin;
            }
            // 否则递归
            return fracToBin(na, bits, bin);
        }
        return bin;
    }

用代码表示十进制转换二进制的整体计算过程:


   /**
    * [DecToBin 十进制转二进制]
    *
    * @param   {[type]}  n  [n description]
    *
    * @return  {[type]}     [return description]
    */
   function DecToBin(n) {
        let inter, frac;
        let arr = ('' + n).split('.');
        inter = arr[0] === '0' ? arr[0] : integerToBin(arr[0]);
        frac = arr[1] ? '.' + fracToBin(parseFloat('0.' + arr[1])) : '';
        return parseFloat([inter, frac].join(''));
   }

   DecToBin(2.3555);

当然,以上代码仅用于展示十进制小数转换为二进制小数的过程,实际开发中,我们可以直接像下面这样转换:

let a = 2.3555;
console.log(a.toString(2));

二进制小数的存储

浮点数

我们将二进制小数算出来还不算完,还要明白计算机中如何存储二进制小数的。

小数,在计算机语言里,准确应该叫做浮点数。

而浮点数根据精确度不同分为很多种,最常用的有两种:

  • 单精度浮点数,采用32位二进制位存储
  • 双精度浮点数,采用64位二进制位存储

浮点数精度

所谓精度,就是二进制数能够表达的数的精确度,在计算机中,二进制的存储存在着浮点数精度丢失的风险。

我们来看下小数点后四位能够用二进制所表示的十进制数:

二进制数对应的十进制数
0.00000
0.00010.0625
0.00100.125
0.00110.1875
0.01000.25
0.01010.3125
0.01100.375
0.01110.4375
0.10000.5
0.10010.5625
0.10100.625
0.10110.6875
0.11000.75
0.11010.8125
0.11100.875
0.11110.9375

这里,左面这一列的二进制数是连续的,它已经穷尽了四位二进制所能表达的所有二进制数

但右边十进制这一列却不是连贯的。

想象一下,我们如何使用四位二进制来展示 0.0715这个数字?

答案是,我们无法用四位二进制来表示,甚至无法用有限的二进制位来表示,按照我们上面介绍过的十进制小数转换为二进制小数的方法,它将得到一个无限小数,然而计算机存储是有上限的,不可能给它无数个位来存储,所以就会将超出最高存储位数上限的部分给截掉,这就是精度丢失的原因。

浮点数存储

根据IEEE 754规范,计算机对浮点数的存储总共分为三部分:

符号位 + 指数位 + 尾数位

每部分的位数根据精度不同而不同,比如

  • 32位单精度浮点数的存储格式: 1位符号位 + 8 位指数位 + 23 位尾数位
  • 64位双精度浮点数的存储格式: 1位符号位 + 11位指数位 + 52位尾数位

上述三部分我们分别解释下:

  • 符号位,代表数字的正负,为【1】时表示【负数】,为【0】时表示【正数或者0】

  • 尾数位

    我们都知道,一个十进制数可以使用多种方式表达,比如3.14这个十进制数,他可以表达为以下几种形式:

    • 314 * 10-2
    • 0.314 * 101
    • 0.00314 * 103

    为了方便计算机处理,科学家们就规定了一个对于十进制数统一的表示规则:小数点前面是0,小数点后面第1位不能是0.

    所以所有十进制数有了相同的表达形式:

    • 3.14 => 0.314 * 101
    • 175 => 0.175 * 103
    • 0.003 => 0.3 * 10-2

    同样地,二进制小数也可以有多种表达形式,比如10.10这个二进制数,可以表达为:

    • 10.10 * 20
    • 1.010 * 21
    • 101.0 * 2-1

    为了计算机方便处理,计算机科学家规定了对于一个二进制小数的表示规则: 将小数点前面的值固定为1,并且确保小数点后面的长度为规定的精度的尾数位数。

    所以所有的二进制小数就有了相同的表达形式(以32位精度为例,其尾数尾数位23位):

    • 10.10 => 1.01000000000000000000000 * 21
    • 0.0010 => 1.00000000000000000000000 * 2-3
    • 100.1 => 1.00100000000000000000000 * 22

    而且既然规定了所有数字的整数各位都是1,那么为了节省存储空间,这个1就可以省略了,最后仅保留小数部分,就是这个二进制小数的尾数,以上面三个为例:

    • 10.10的尾数: 01000000000000000000000;
    • 0.0010的尾数: 00000000000000000000000;
    • 100.1 的尾数: 00100000000000000000000;
  • 指数位

    • 指数位采用EXCESS系统表现

    • EXCESS 系统表现是指,通过将指数部分表示范围的中间值设为 0,使得负数不 需要用符号来表示

    • 拿扑克牌举例,比如我们有从A到K十三张扑克牌,现在我们以中间的 7 为 0,则根据EXCESS系统,形成了以下对应关系:

      牌面值EXCESS系统值
      A-6
      2-5
      3-4
      4-3
      5-2
      6-1
      70
      81
      92
      103
      J4
      Q5
      K6
    • 同样地,当精度为32位时,指数位为8位,它能表示的最大二进制数为11111111,即255,

    • 我们取它中间的数,即使用11111111 除以2,得到二进制数01111111(二进制中,一个数除以2实际上就是右移一位,左面补0),01111111的十进制数对应的是127,

    • 根据EXCESS系统要求,我们将中间值127表示为0, 则最终会形成类似下面的表示

      二进制值十进制值EXCESS值
      ………………
      1111100124-3
      1111101125-2
      1111110126-1
      011111111270
      100000001281
      100000011292
      100000101303
      ………………
    • 所以上例中的三个数字的对应指数位分别是:(使用小数通用表示规则写出后,指数是2的n次方,这里的n代表EXCESS值,而指数位存储的则是对应的二进制值)

      • 10.10的指数位:10000000
      • 0.0010的指数位:1111100
      • 100.1的指数位: 10000001

    所以,上面三个小数10.100.0010100.1,最终的二进制存储为(按照32位精度):

    二进制小数统一表达正负指数符号位指数位尾数位
    10.101.01000000000000000000000 * 21101000000001000000000000000000000
    0.00101.00000000000000000000000 * 2-3-30111110000000000000000000000000
    100.11.00100000000000000000000 * 22201000000100100000000000000000000

    最终存储的二进制值就是 符号位+指数位+尾数位。

    上表是32位精度下的存储,64位精度时的尾数为52位,指数中间值是01111111111 (即十进制的1023)对应EXCESS系统的0。其它以此类推。

二进制知识实战巩固

我们学习了以上知识后,为了检验我们是否掌握,就拿javascript中最经典的0.1+0.2 !=== 0.3 为例,来分析一下其中的原因。

首先,我们要知道,javascript 存储的二进制数是64位精度的

1.转换

首先,我们分别将0.10.2转换为二进制小数,可以利用我们上面学过的转换方法,得到的结果是(保留了60位小数):

十进制小数二进制小数
0.10.000110011001100110011001100110011001100110011001100110011001
0.20.001100110011001100110011001100110011001100110011001100110011

2.改写为统一表达式

我们将二进制小数改写为统一表达式,由于统一表达式要求小数点后的位数要和当前精度(64)位的尾数位数一致(52位),而我们的二进制小数都保留了60位,即使经过改写为统一表达式后左移了几位,但还是多于52位,所以多余部分的我们要截去。

二进制小数统一表达式
0.0001100110011001100110011001100110011001100110011001100110011.1001100110011001100110011001100110011001100110011001 * 2-4
0.0011001100110011001100110011001100110011001100110011001100111.1001100110011001100110011001100110011001100110011001 * 2-3

注意:截取的时候不是直接去掉,而是为最大限度保留精度,采取 【0舍1入】的规则

上面两个二进制小数的小数部分第53位都为1,所以舍去的时候要加1,

即上面两个表达式其实不是最终表达式,最终表达式要在尾数部分+1,得到:

  • 0.1 => 1.1001100110011001100110011001100110011001100110011010 * 2-4

  • 0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2-3

3.获得最终存储值

我们要分别转换为统一表达式后的符号位、指数位、尾数位

这里,尾数可以直接从表达式里拿到,符号位都是0(正数),唯一剩余的就是获取指数位了

在64位精度下,指数位位11位,所以最大二进制值为1111 1111 111,取中间数,即除以2,右移一位,左位补零,得到0111 1111 111 (十进制1023)

然后根据EXCESS系统规则列出它前后的对应值:

二进制值十进制值EXCESS系统值(指数值)
011 1111 10111019-4
011 1111 11001020-3
011 1111 11011021-2
011 1111 11101022-1
0111 1111 11110230
100 0000 000010241
100 0000 000110252
100 0000 001010263
100 0000 00111027……

那么0.10.2的指数位分别是EXCESS系统-4-3所对应的值:

  • 0.1 指数位:011 1111 1011
  • 0.2 指数位: 011 1111 1100

由此,获得0.10.2的二进制最终存储值:

十进制值二进制存储值
0.10 01111111011 1001100110011001100110011001100110011001100110011010
0.20 01111111100 1001100110011001100110011001100110011001100110011010

计算和

现在,我们将上表中两个二进制存储值进行相加,逢二进一,得到二进制存储结果:

0 0111 1111 101 0011001100110011001100110011001100110011001100110100

那么我们如何将它还原回二进制进而还原回十进制小数呢?

逆向工程开始:

  • 第1位0,代表正数
  • 第2到12位,代表指数,0111 1111 101 对照上面的EXCESS系统表,是十进制的1021,对应指数位-2
  • 第13位到64位,0011001100110011001100110011001100110011001100110100 代表尾数
  • 还原为统一表达式: 1.0011001100110011001100110011001100110011001100110100 * 2-2
  • 得到无指数形式: 0.010011001100110011001100110011001100110011001100110100

然后我们利用第一节的二进制小数转换十进制的方法,得到结果:

image-20210927180509588

所以,在javascript中,0.1 + 0.2 === 0.30000000000000004 会返回true;

image-20210927180623246

,是十进制的1021,对应指数位-2

  • 第13位到64位,0011001100110011001100110011001100110011001100110100 代表尾数
  • 还原为统一表达式: 1.0011001100110011001100110011001100110011001100110100 * 2-2
  • 得到无指数形式: 0.010011001100110011001100110011001100110011001100110100

然后我们利用第一节的二进制小数转换十进制的方法,得到结果:

[外链图片转存中…(img-7q6d05kg-1632739132361)]

所以,在javascript中,0.1 + 0.2 === 0.30000000000000004 会返回true;

[外链图片转存中…(img-Msu3JYWH-1632739132363)]

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值