big.js设计思路和源码分享

前言

众所周知,计算机进行数字运算的时候会把十进制转化为二进制,因此会产生0.1+0.2=0.30000000004 精度丢失的问题。一般来说,我们会采用math.js等第三方库的方式解决,不过我之前也没去了解过它们的原理。

有一天,我就想,如果我们就像小学学数学一样把数字放在十进制层面进行运算,不就不会出现这个问题了吗,然后我简单地写了一个demo

function plus (a, b) {
    const str1 = String(a).split('.')
    let dot1 = str1[1]?.split('') ?? [ 0 ]
    dot1 = dot1.map(item => Number(item))

    const str2 = String(b).split('.')
    let dot2 = str2[1]?.split('') ?? [ 0 ]
    dot2 = dot2.map(item => Number(item))

    let count = Number(str1[0]) + Number(str2[0])
    const newArr = []
    const length = Math.max(dot1.length, dot2.length)
    for (let i = length - 1; i >= 0; i--) {
        let num = (dot1[i] ?? 0) + (dot2[i] ?? 0) + (newArr[i] ?? 0)
        if (num >= 10) {
            num -= 10
            if (i > 0) {
                newArr[i - 1] = 1
            } else {
                count++
            }
        }
        newArr[i] = num
    }
    return Number(count + '.' + newArr.join(''))
}

输入plus(0.1,0.2),先将数字转字符串解析,进行十进制加减法,最后转数字输出0.3

当然,我有很多边界条件没有考虑,这仅仅是个demo

big.js解析整体流程

然后我就去看了一下big.js的源码,确实就是这种解决方案,不过这个库并不是像我一样只是去解决加法问题,它通过构造函数创建了一个Big对象,然后这个对象中包含了很多数学信息,原型上也挂了很多方法,用于进行各种运算。

这是它的整体结构,首先闭包全局注入或在导出了一个Big构造函数

(function (GLOBAL) {
    'use strict';
    var Big
    function _Big_ () {
        function Big (n) {
            // 构造函数,下文会进行分析
        }
        Big.prototype = P
        // 等一些原型方法
        return Big
    }

    // Export
    Big = _Big_()
    Big['default'] = Big.Big = Big
  
    //AMD.
    if (typeof define === 'function' && define.amd) {
        define(function () {
            return Big
        })

    // Node and other CommonJS-like environments that support module.exports.
    } else if (typeof module !== 'undefined' && module.exports) {
        module.exports = Big

    // Browser.
    } else {
        GLOBAL.Big = Big
    }
})(this)

全局注入后,当我们运行Big()或new Big()时

function _Big_() {

    /*
     * The Big constructor and exported function.
     * Create and return a new instance of a Big number object.
     *
     * n {number|string|Big} A numeric value.
     */
    function Big(n) {
      var x = this; // new Big()的this是Big(),Big()的this是Window

      // Enable constructor usage without new. 
      // 可以不通过构造函数使用,它会帮你转化为new
      if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);

      // Duplicate.
      // 如果传进来的就是Big对象,拷贝一份
      if (n instanceof Big) {
        x.s = n.s;
        x.e = n.e;
        x.c = n.c.slice();
      } else {
        if (typeof n !== 'string') {
          // 严格模式只能使用字符串和bigInt,否则报错
          if (Big.strict === true && typeof n !== 'bigint') {
            throw TypeError(INVALID + 'value');
          }

          // Minus zero?
          // -0的情况也考虑到了,因为String(-0) => '0'
          n = n === 0 && 1 / n < 0 ? '-0' : String(n);
        }
  
        // 前置校验结束后,解析字符串
        parse(x, n);
      }

      // Retain a reference to this Big constructor.
      // Shadow Big.prototype.constructor which points to Object.
      x.constructor = Big;
    }

    Big.prototype = P;
    Big.DP = DP;
    Big.RM = RM;
    Big.NE = NE;
    Big.PE = PE;
    Big.strict = STRICT;
    Big.roundDown = 0;
    Big.roundHalfUp = 1;
    Big.roundHalfEven = 2;
    Big.roundUp = 3;

    return Big;
  }

在非常细致地前置考虑了如何处理特殊入参之后,big.js调用了parse的方法进行解析

function parse(x, n) {
    // 此时的x是Big()构造函数产生的对象,n是字符串
    var e, i, nl;
    // NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
    // 首先会正则校验字符串是否合法
    if (!NUMERIC.test(n)) {
      throw Error(INVALID + 'number');
    }

    // Determine sign. 正负
    x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1;

    // Decimal point? 小数点位置
    if ((e = n.indexOf('.')) > -1) n = n.replace('.', '');

    // Exponential form? 科学计数法,如 5e3 => 5000
    if ((i = n.search(/e/i)) > 0) {

      // Determine exponent.
      if (e < 0) e = i;
      e += +n.slice(i + 1);
      n = n.substring(0, i);
    } else if (e < 0) {

      // Integer.
      e = n.length;
    }

    nl = n.length;

    // 将0123.1230转化为123.123
    // Determine leading zeros. 
    for (i = 0; i < nl && n.charAt(i) == '0';) ++i;

    if (i == nl) {

      // Zero.
      x.c = [x.e = 0];
    } else {

      // Determine trailing zeros.
      for (; nl > 0 && n.charAt(--nl) == '0';);
      x.e = e - i - 1;
      x.c = [];

      // Convert string to array of digits without leading/trailing zeros.
      for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
    }

    return x;
  }

通过这一系列的解析,最终的产生的对象如图

c是去前后0后的数字数组

e是小数点所在位置

s是正负,正为1,负为-1

这个思路整体是和我上面写的demo是相似的,我们来看一下原型上的方法来验证一下

原型上的方法举例:加法和乘法

/*
   * Return a new Big whose value is the value of this Big plus the value of Big y.
   */
  P.plus = P.add = function (y) {
    var e, k, t,
      x = this,
      Big = x.constructor;

    y = new Big(y);

    // Signs differ?
    if (x.s != y.s) {
      y.s = -y.s;
      return x.minus(y);
    }

    var xe = x.e,
      xc = x.c,
      ye = y.e,
      yc = y.c;

    // Either zero?
    if (!xc[0] || !yc[0]) {
      if (!yc[0]) {
        if (xc[0]) {
          y = new Big(x);
        } else {
          y.s = x.s;
        }
      }
      return y;
    }

    xc = xc.slice();

    // Prepend zeros to equalise exponents.
    // Note: reverse faster than unshifts.
    if (e = xe - ye) {
      if (e > 0) {
        ye = xe;
        t = yc;
      } else {
        e = -e;
        t = xc;
      }

      t.reverse();
      for (; e--;) t.push(0);
      t.reverse();
    }

    // Point xc to the longer array.
    if (xc.length - yc.length < 0) {
      t = yc;
      yc = xc;
      xc = t;
    }

    e = yc.length;

    // Only start adding at yc.length - 1 as the further digits of xc can be left as they are.
    for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;

    // No need to check for zero, as +x + +y != 0 && -x + -y != 0

    if (k) {
      xc.unshift(k);
      ++ye;
    }

    // Remove trailing zeros.
    for (e = xc.length; xc[--e] === 0;) xc.pop();

    y.c = xc;
    y.e = ye;

    return y;
  };

通过上述的代码中,我们可以看到,加法操作其实就是在符号相同的情况下,对齐两个数字的小数点,然后对数组中的每一对数据进行加法操作,得到结果后再保存下来。

/*
   * Return a new Big whose value is the value of this Big times the value of Big y.
   */
  P.times = P.mul = function (y) {
    var c,
      x = this,
      Big = x.constructor,
      xc = x.c,
      yc = (y = new Big(y)).c,
      a = xc.length,
      b = yc.length,
      i = x.e,
      j = y.e;

    // Determine sign of result.
    y.s = x.s == y.s ? 1 : -1;

    // Return signed 0 if either 0.
    if (!xc[0] || !yc[0]) {
      y.c = [y.e = 0];
      return y;
    }

    // Initialise exponent of result as x.e + y.e.
    y.e = i + j;

    // If array xc has fewer digits than yc, swap xc and yc, and lengths.
    if (a < b) {
      c = xc;
      xc = yc;
      yc = c;
      j = a;
      a = b;
      b = j;
    }

    // Initialise coefficient array of result with zeros.
    for (c = new Array(j = a + b); j--;) c[j] = 0;

    // Multiply.

    // i is initially xc.length.
    for (i = b; i--;) {
      b = 0;

      // a is yc.length.
      for (j = a + i; j > i;) {

        // Current sum of products at this digit position, plus carry.
        b = c[j] + yc[i] * xc[j - i - 1] + b;
        c[j--] = b % 10;

        // carry
        b = b / 10 | 0;
      }

      c[j] = b;
    }

    // Increment result exponent if there is a final carry, otherwise remove leading zero.
    if (b) ++y.e;
    else c.shift();

    // Remove trailing zeros.
    for (i = c.length; !c[--i];) c.pop();
    y.c = c;

    return y;
  };

上面是乘法,其实乘法的本质和加法也是类似的,每一位数字进行运算后再保存回原数组即可。也就是我们小学学过的乘法计算方式。

其他

最后提一下Number()、String()等强制转换的操作

回忆一下我们学过的知识,如何重写强制转换?就是通过重写toString(),搜一下源码中的toString,确实是如此实现的

/*
   * Return a string representing the value of this Big.
   * Return exponential notation if this Big has a positive exponent equal to or greater than
   * Big.PE, or a negative exponent equal to or less than Big.NE.
   * Omit the sign for negative zero.
   */
  P.toJSON = P.toString = function () {
    var x = this,
      Big = x.constructor;
    return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]);
  };

结语

总结了几个点

在big.js的源码中,big.js将数字转换为三个属性:正负、数字的字符串、小数点位置,然后对每一位进行十进制运算

在每一个运算函数中,big.js都会先进行异常检测,然后对数据进行处理,如果遇到不符合处理逻辑的数值,都会转化为符合要求的情况。这样,代码看起来条理清晰,思路明确,不需要通过不同的逻辑处理代码来处理不同类型的数据

在es10中,也有新的数据类型BigInt,相比big.js等数学库来说,优势肯定是性能更好(不过可以忽略不计),缺点就是兼容性问题并且BigInt和Number类型不可一起运算,BigInt也不支持Math对象中的方法

非常建议在大数运算或小数运算的时候使用math.js或big.js等第三方库

github链接:https://github.com/MikeMcl/big.js/blob/9c6c959c92dc9044a0f98c31f60322fd91243468/big.js#L261

参考:https://blog.csdn.net/weixin_42899690/article/details/120557100

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值