JavaScriptの大数运算のこと

事先说明,大数运算并不只是指特别大的数进行运算,而是广泛指运算超出了运算精度。
那么什么情况会超出运算精度呢,特别大的数也不常遇见,就 0.1 + 0.2 ,可以随便打开一个控制台,输入这个语句,可以发现返回值是 0.30000000000000004 ,但直接输入 0.3 并不会有这个问题。

image

这是因为 js 在进行小数运算的时候,可能会丢失一些精度,导致计算结果与真实值之间存在误差。
那么如何解决这个问题呢,框架自然是最好用的,但在分析框架源码之前,先提几句我个人的思路。
js 中,像 number、string、boolean 这种原生数据类型,都是存在原生对象的,可以使用原生对象做数据类型的转化。

let a = 0.1, b = 0.2;
a = String(a);
b = String(b);

将数字转化成字符串后,字符串是不会丢失精度的,而且 js 中字符串同样可以用 for in 遍历,要做的不过是像小学生一样,将小数点对齐,按位一一进行运算罢了。
但是要实现上述运算的话,我的办法就比框架愚拙了不少,所以现在放下这个思路,看看框架是如何解析数字的。
这里推荐的框架是 big.js ,这个框架还有姐妹框架,比如 bignumber.js 增加了对非十进制数的运算,以及 decimal.js 增加了对非整数幂的支持。

完整源码还请访问官方 cdn,内容太多我就不贴到博客里了。

(function (GLOBAL) {
  // 内部定义的函数内容先省略
  var Big;
  Big = _Big_();
  Big["default"] = Big.Big = Big;
  if (typeof define === "function" && define.amd) {
    define(function () {
      return Big;
    });
  } else if (typeof module !== "undefined" && module.exports) {
    module.exports = Big;
  } else {
    GLOBAL.Big = Big;
  }
})(this);

首先源码是一个自调用函数,将 this 传入,并用参数 GLOBAL 接收,在浏览器中 this 指向全局的 window 对象,而在 node 中,this 则会指向 module.exports ,具体原因请看我的这篇博客《痛觉残留——Node 原理》。

在函数内,第一步是定义 Big 变量,赋值为 _Big_ 的执行结果,函数内容稍后再看,先知道函数返回了一个构造函数,然后在 Big 对象上 做了一层循环引用,即 Big 的 default 属性和 Big 属性都指向自己。
最后三个条件块兼容三种不同的导入框架。

  1. 第一种是 AMD 导入模块的规范,在 ES5 时浏览器本身不支持模块化,使用该规范就需要导入相应框架,而在这类框架中会实现 define 函数用来导入模块,现在 AMD 规范已经很少被使用了可以忽略。
  2. 第二种是 Node 或者遵循 CommonJS 规范的导入模块方式。
  3. 第三钟就是通过 script 标签导入,会在全局 window 对象上挂在 Big 属性,值是 Big 对象。

接下来看 Big 函数,返回值 Big 函数会挂载在 window 的 Big 属性上,这样每次使用相当于调用 Big 函数,同时将 Big 的 prototype 指向空对象 P,之后 Big 实例的方法将会定义在 P 上。

var P = {}, STRICT = false, UNDEFINED = void 0;
function _Big_() {
  function Big(n) {
    var x = this;
    if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);
    if (n instanceof Big) {
      x.s = n.s;
      x.e = n.e;
      x.c = n.c.slice();
    } else {
      if (typeof n !== "string") {
        if (Big.strict === true) {
          throw TypeError(INVALID + "number");
        }
        n = n === 0 && 1 / n < 0 ? "-0" : String(n);
      }
      parse(x, n);
    }
    x.constructor = Big;
  }
  Big.prototype = P;
  Big.DP = DP;
  Big.RM = RM;
  Big.NE = NE;
  Big.PE = PE;
  Big.strict = STRICT;
  return Big;
}

首先说明,void 是一元运算符,它可以出现在任意类型的表达式前执行,却忽略表达式的返回值,返回一个 undefined。
官方文档中介绍,使用 big.js 有两种方法,一是作为类使用 new 操作符,二是作为函数使用。作为函数使用时,this 的值是 undefined,此时 x instanceof Big 为 false,同时如果传入实参,n === UNDEFINED 也为 false,于是返回 new Big(n),相当于即使通过函数使用,框架会重新替我们以 new 操作符初始化;而当我们通过 new 初始化时,this 指向 Big 的实例,x instanceof Big 则为 true。
下一步判断 n 是否也是 Big 的实例,如果是,会对 n 的属性做深拷贝,属性值所代表的意义之后介绍;而如果 n 不是 Big 的实例,则会判断 n 的类型,由于字符串类型不会丢失精度,所以如果 n 不是字符串类型,且 Big 是严格模式,则会抛出一个类型错误,而 Big 默认为非严格模式,所以 n 会被转化为字符串类型。特别说明 n 转化成字符串类型前特意对-0 做了单独处理,因为只有-0 === 0 且 1 / -0 = -Infinity < 0。
最后将 this 和 n 传入 parse 函数。

var NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
function parse(x, n) {
  var e, i, nl;
  if (!NUMERIC.test(n)) {
    throw Error(INVALID + "number");
  }
  x.s = n.charAt(0) == "-" ? ((n = n.slice(1)), -1) : 1;
  if ((e = n.indexOf(".")) > -1) n = n.replace(".", "");
  if ((i = n.search(/e/i)) > 0) {
    if (e < 0) e = i;
    e += +n.slice(i + 1);
    n = n.substring(0, i);
  } else if (e < 0) {
    e = n.length;
  }
  nl = n.length;
  for (i = 0; i < nl && n.charAt(i) == "0"; ) ++i;
  if (i == nl) {
    x.c = [(x.e = 0)];
  } else {
    for (; nl > 0 && n.charAt(--nl) == "0"; );
    x.e = e - i - 1;
    x.c = [];
    for (e = 0; i <= nl; ) x.c[e++] = +n.charAt(i++);
  }
  return x;
}

parse 函数首先用正则检验传入的 n 是否是数字,匹配包含正数负数、整数小数以及无整数部分小数(如.123,相当于 0.123),还会匹配超出数字表示范围采用科学计数法的数字(包含 e),若传入数字不合规范就会抛出错误。
charAt 是取到字符串对应下标的字符,也可直接用 string[index] 这种方式。判断第一个字符是否为-号,若是,删除原字符串的负号并将 Big 实例的 s 属性置为-1;若不是,则 s 属性置为 1。然后判断有无小数部分,若有,将 e 赋值为小数点所在小标并删除小数点,若无,e 的值赋为-1。继续判断数字中是否含 e,这种情况相对少见,如对科学计数法不熟悉可以忽略。
变量 e 的值域为[-1, +∞),用 e<0 判断相对 e===-1 严谨一些,如果 e 为-1,则说明数字只包含整数部分,因此小数点所在下标即为数字长度,并将无论是否包含小数的数字长度另存为 nl 变量。
考虑到传入小数时,存在整数位数和小数前几位都为 0 的情况,为方便后续运算,先将前置 0 的个数赋值给 i(注意:0.00102 中 i 的值为 3,.00102 中 i 值为 2)。
i==nl 的情况就是数字全是 0,相当于 n 数值为 0,框架考虑还挺全面的,我是想不到什么情况会传一串 0 进来。而 Big 实例属性 c 的数组则用来储存每一位数字,0 就只存一个 0。e 属性存 0 的原因稍后解释。
n 不为 0 时,需要过滤尾置 0,先将 nl 减去尾置 0 的个数。通过 for 循环将去除前置 0 和尾置 0 的数字部分存入属性 c 中,+号作用是将字符串类型转化为数字类型,x.e 存的是小数点下标减去前置 0 个数再减 1,这样当恢复数字的原数值时,只需将属性 c 中的数字乘以 10 的 x.e-index 次方(index 为数字再 c 中的下标),建议多举几个例子就明白了。
至此,Big 实例就通过这样一种非常简单的方式存储了 x 所有必要信息,并以此为基准实现了诸多计算与比较方法,虽然那些函数也很值得分析,但我写不动了。

你可能感兴趣的:(JavaScriptの大数运算のこと)