翼度科技»论坛 编程开发 JavaScript 查看内容

图文剖析 big.js 四则运算源码

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
big.js,一个小型、快速的用于任意精度的十进制算术的JavaScript 库。
big.js 用于解决平常项目中进行算术运算时精度丢失引起的结果不准确的问题。和 big.js 类似的两个库 bignumber.js 和 decimal.js 也都是出自同一作者(MikeMcl)之手。
作者在 这里 详细说明了他们之间的区别
big.js 是最小的任意精度的计算库。big.js 是三者中最小也最简单的,它只有 bignumber.js 一半的方法,不到 bignumber.js 的一半大。
bignumber.js 和 decimal.js 存储值的进制比 big.js 更高,因此当操作大量数字时,前两者的速度会更快。
bignumber.js 可能更适合金融类应用,因为用户不用担心丢失精度,除非使用了涉及除法的操作。
这篇文章分别就 big.js 的解析函数,以及加减乘除运算的源码进行剖析,了解作者的设计思路。在四则运算的源码中,相比加减乘,除法运算最为复杂。
用法

创建 Big 对象时,new 操作符是可选的
  1. x = new Big(123.4567)
  2. y = Big('123456.7e-3')                 // 'new' is optional
  3. z = new Big(x)
  4. x.eq(y) && x.eq(z) && y.eq(z)          // true
复制代码
构造函数

构造函数中关键代码如下
  1. function Big(n) {
  2.   var x = this;
  3.   // 使用构造函数前面可以不带 new 关键字
  4.   if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);
  5.   // 如果传进来的参数已经是 Big 的实例对象,则复制一份,否则使用 parse 函数创建一个实例对象
  6.   if (n instanceof Big) {
  7.     x.s = n.s;
  8.     x.e = n.e;
  9.     x.c = n.c.slice();
  10.   } else {
  11.     if (typeof n !== 'string') {
  12.       if (Big.strict === true && typeof n !== 'bigint') {
  13.         throw TypeError(INVALID + 'value');
  14.       }
  15.       // 传入的如果是 -0 ,则转为字符串表示 '-0'
  16.       n = n === 0 && 1 / n < 0 ? '-0' : String(n);
  17.     }
  18.     parse(x, n);
  19.   }
复制代码
使用构造函数前面可以不带 new 关键字
如果传进来的参数已经是 Big 的实例对象,则将实例对象的属性复制一份,否则使用 parse 函数为实例对象创建属性。
parse 函数
  1. function parse(x, n) {
  2.     var e, i, nl;
  3.     // NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
  4.     if (!NUMERIC.test(n)) {
  5.       throw Error(INVALID + 'number');
  6.     }
  7.     // Determine sign.
  8.     x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1;
  9.     // Decimal point?
  10.     if ((e = n.indexOf('.')) > -1) n = n.replace('.', '');
  11.     // Exponential form?
  12.     if ((i = n.search(/e/i)) > 0) {
  13.       // Determine exponent.
  14.       if (e < 0) e = i;
  15.       e += +n.slice(i + 1);
  16.       n = n.substring(0, i);
  17.     } else if (e < 0) {
  18.       // Integer.
  19.       e = n.length;
  20.     }
  21.     nl = n.length;
  22.     // Determine leading zeros.
  23.     for (i = 0; i < nl && n.charAt(i) == '0';) ++i;
  24.     if (i == nl) {
  25.       // Zero.
  26.       x.c = [x.e = 0];
  27.     } else {
  28.       // Determine trailing zeros.
  29.       for (; nl > 0 && n.charAt(--nl) == '0';);
  30.       x.e = e - i - 1;
  31.       x.c = [];
  32.       // Convert string to array of digits without leading/trailing zeros.
  33.       for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
  34.     }
  35.     return x;
  36.   }
复制代码

  • if ((e = n.indexOf('.')) > -1) n = n.replace('.', ''); 是否含有小数点,如果是,则删除小数点,并将 e 的初始值设为小数点的位置
  1. 1234 会被转化为
  2. {
  3.     c:[1,2,3,4],
  4.     e:3,
  5.     s:1
  6. }
复制代码

  • 如果数字是科学表示法,比如 100e2 ,e 的位置是 3,e 后面的指数是 2 ,则 x.e = 3 + 2
  1. // NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
  2. if (!NUMERIC.test(n)) {
  3.   throw Error(INVALID + 'number');
  4. }
  5. Big('123400'),Big('-0.1234'),Big('100e2') 都通过
复制代码

  • nl = n.length; nl 表示传进来的数字的长度
  1. Big('123400') => x.s = 1
  2. Big('-0.1234') => x.s = -1   并且 -0.1234 => 0.1234
  3. Big('100e2') => x.s = 1
复制代码

  • for (i = 0; i < nl && n.charAt(i) == '0';) ++i; 确定数字是否有前置 0 ,这里的 i 表示第一个不为 0 的数字的位置,也可以表示数字前面有多少个 0
  1. Big('123400') => x.e = -1 , n = 123400
  2. Big('-0.1234') => x.e = 1 , n = 01234
  3. Big('100e2') => x.e = -1 , n = 100e2
复制代码

  • 如果 i = nl,则说明传进来的输入是一个 0 或者多个 0
  1. if ((i = n.search(/e/i)) > 0) {
  2.   // Determine exponent.
  3.   if (e < 0) e = i;
  4.   e += +n.slice(i + 1);
  5.   n = n.substring(0, i);
  6. } else if (e < 0) {
  7.   // Integer.
  8.   e = n.length;
  9. }
  10. Big('123400')
  11. x.e = -1  =>  x.e = n.length = 6
  12. n = 123400
  13. Big('-0.1234')
  14. x.e = 1
  15. n = 01234
  16. Big('100e2')
  17. x.e = -1  =>  x.e = e 在 100e2 中的位置 + e 后面紧跟的指数系数 = 3 + 2 = 5
  18. n = 100e2  =>  n = 100
复制代码
最后 Big('123400'),Big('-0.1234'),Big('100e2') 将转换为
  1. Big('123400')
  2. x.e = 6
  3. n = 123400
  4. nl = 6
  5. Big('-0.1234')
  6. x.e = 1
  7. n = 01234
  8. nl = 5
  9. Big('100e2')
  10. x.e = 5
  11. n = 100
  12. nl = 3
复制代码
至此 parse 函数逻辑结束,接下来分别剖析下加减乘除运算;
加法

源码
  1. Big('123400')
  2. x.e = 6
  3. n = 123400
  4. nl = 6
  5. i = 0
  6. Big('-0.1234')
  7. x.e = 1
  8. n = 01234
  9. nl = 5
  10. i = 1
  11. Big('100e2')
  12. x.e = 5
  13. n = 100
  14. nl = 3
  15. i = 0
复制代码

  • 如果符号不同,则转为减法运算;比如 -x + y 就是 y - x ,x + -y 就是 x - y
  1. if (i == nl) {
  2.   // Zero.
  3.   x.c = [x.e = 0];
  4. } else {
  5.   // 排除尾随 0,nl 为最后一个不为 0 的数字的位置
  6.   for (; nl > 0 && n.charAt(--nl) == '0';);
  7.   x.e = e - i - 1;
  8.   x.c = [];
  9.   // 传进来的数字,排除掉前置 0 和尾随 0 后,转换为数字数组
  10.   for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
  11. }
  12. Big('123400')
  13. //因为默认 e 是 n.length,而Big指数表示是 1.234 * 10^5,所以这里 x.e 要减一
  14. x.e = 6  =>  x.e = e - i - 1 = 6 - 0 - 1 = 5  
  15. n = 123400
  16. nl = 6  =>  排除尾随 0,nl 为最后一个不为 0 的数字的位置  =>  nl = 3
  17. i = 0
  18. x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1,2,3,4]
  19. Big('-0.1234')
  20. x.e = 1  =>  x.e = e - i - 1 = 1 - 1 - 1 = -1
  21. n = 01234
  22. nl = 5  =>  排除尾随 0,nl 为最后一个不为 0 的数字的位置  =>  nl = 4
  23. i = 1
  24. x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1,2,3,4]
  25. Big('100e2')
  26. x.e = 5  =>  x.e = e - i - 1 = 5 - 0 - 1 = 4  
  27. n = 100
  28. nl = 3  =>  排除尾随 0,nl 为最后一个不为 0 的数字的位置  =>  nl = 0
  29. i = 0
  30. x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1]
复制代码

  • 其中两个数字是不是 0,其中有一个为 0,则直接返回另外一个
  1. Big('123400')
  2. x.s = 1
  3. x.e = 5
  4. x.c = [1,2,3,4]
  5. Big('-0.1234')
  6. x.s = -1
  7. x.e = -1
  8. x.c = [1,2,3,4]
  9. Big('100e2')
  10. x.s = 1
  11. x.e = 4
  12. x.c = [1]
复制代码

  • 比较指数幂差,较小的一方,在前面补零,方便后续加法操作;并且将指数幂较大的一方,作为两数相加的结果的指数幂的初始值。
  1. P.plus = P.add = function (y) {
  2.     var e, k, t,
  3.       x = this,
  4.       Big = x.constructor;
  5.     y = new Big(y);
  6.     // 校验符号是否不同
  7.     if (x.s != y.s) {
  8.       y.s = -y.s;
  9.       return x.minus(y);
  10.     }
  11.     var xe = x.e,
  12.       xc = x.c,
  13.       ye = y.e,
  14.       yc = y.c;
  15.     // 校验是否是 0
  16.     if (!xc[0] || !yc[0]) {
  17.       if (!yc[0]) {
  18.         if (xc[0]) {
  19.           y = new Big(x);
  20.         } else {
  21.           y.s = x.s;
  22.         }
  23.       }
  24.       return y;
  25.     }
  26.     xc = xc.slice();
  27.     // 前面加上零使指数均衡
  28.     // Note: reverse faster than unshifts.
  29.     if (e = xe - ye) {
  30.       if (e > 0) {
  31.         ye = xe;
  32.         t = yc;
  33.       } else {
  34.         e = -e;
  35.         t = xc;
  36.       }
  37.       t.reverse();
  38.       for (; e--;) t.push(0);
  39.       t.reverse();
  40.     }
  41.     // 让 xc 存放长度更长的数字
  42.     if (xc.length - yc.length < 0) {
  43.       t = yc;
  44.       yc = xc;
  45.       xc = t;
  46.     }
  47.     e = yc.length;
  48.     for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;
  49.     // No need to check for zero, as +x + +y != 0 && -x + -y != 0
  50.     if (k) {
  51.       xc.unshift(k);
  52.       ++ye;
  53.     }
  54.     // 删除尾随 0
  55.     for (e = xc.length; xc[--e] === 0;) xc.pop();
  56.     y.c = xc;
  57.     y.e = ye;
  58.     return y;
  59.   };
复制代码

  • xc 存放长度更长的数字
  1. if (x.s != y.s) {
  2.   y.s = -y.s;
  3.   return x.minus(y);
  4. }
复制代码

  • 接下来是加法逻辑
  1. if (!xc[0] || !yc[0]) {
  2.   if (!yc[0]) {
  3.     if (xc[0]) {
  4.       y = new Big(x);
  5.     } else {
  6.       y.s = x.s;
  7.     }
  8.   }
  9.   return y;
  10. }
复制代码
k 保存进位的值

  • 初始化进位值为 0 ,e 为 yc 长度,执行下面循环体
  • (xc[--e] = xc[e] + yc[e] + k) 计算 xc[e] 加上 yc[e] 加上上一次计算结果进位的值;
  • 随后 xc[--e] 保存计算后的进位的数值,e--
  • 最后 xc[e] 保存计算后的个位数值
上面过程用图例表示如下

减法

源码
  1. if (e = xe - ye) {
  2.   if (e > 0) {
  3.     ye = xe; // 将指数幂较大的一方,作为两数相加的结果的指数幂的初始值
  4.     t = yc;
  5.   } else {
  6.     e = -e;
  7.     t = xc;
  8.   }
  9.   t.reverse();
  10.   for (; e--;) t.push(0);
  11.   t.reverse();
  12. }
  13. 比如 1234 + 12
  14. 1234 在实例对象上是以数字数组形式表示 [1,2,3,4]
  15. 12 则是 [1,2]
  16. 为方便后续数组按照位置进行加法运算,这里需要给 12 补零
  17. [1,2,3,4]
  18.     +
  19. [0,0,1,2]
复制代码
减法前面的逻辑和加法类似,这里不再赘述,已在上面代码注释中说明,下面是减法的核心逻辑
  1. if (xc.length - yc.length < 0) {
  2.   t = yc;
  3.   yc = xc;
  4.   xc = t;
  5. }
复制代码
上面过程用图例表示如下,xc 表示被减数,yc 表示减数
1、若 xc 末尾项大于等于 yc 末尾项,比如 [1,2,3]和[0,0,2],则直接相减。

2、若 xc 末尾项小于 yc 末尾项,则执行以下逻辑
  1. e = yc.length;
  2. for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;
  3. if (k) {
  4.   xc.unshift(k);
  5.   ++ye;
  6. }
  7. // 删除尾随 0
  8. for (e = xc.length; xc[--e] === 0;) xc.pop();
复制代码
上面代码表示从 当前进行相减运算的元素的位置(j) 往前遍历被减数 xc 每个元素,当元素值为 0 时,将值改为 9,直至上一个元素值不为 0 ,循环结束。

至此,减法逻辑结束。
乘法

源码
  1. P.minus = P.sub = function (y) {
  2.     var i, j, t, xlty,
  3.       x = this,
  4.       Big = x.constructor,
  5.       a = x.s,
  6.       b = (y = new Big(y)).s;
  7.     // 确定符号,x - (-y) = x + y    - x - y = -x + (-y)
  8.     if (a != b) {
  9.       y.s = -b;
  10.       return x.plus(y);
  11.     }
  12.     var xc = x.c.slice(),
  13.       xe = x.e,
  14.       yc = y.c,
  15.       ye = y.e;
  16.     // 判断是否为 0
  17.     if (!xc[0] || !yc[0]) {
  18.       if (yc[0]) {
  19.         y.s = -b;
  20.       } else if (xc[0]) {
  21.         y = new Big(x);
  22.       } else {
  23.         y.s = 1;
  24.       }
  25.       return y;
  26.     }
  27.     // 比较两数指数幂大小,给指数幂小的一方补零,方便后续相减;
  28.     // 比如 1234 - 23  parse函数解析后 => [1,2,3,4] - [2,3]  为了使 [2,3] 对应十位,个位
  29.     // 在前面补 0 ,即 [1,2,3,4] - [0,0,2,3]
  30.     // 再比如 66 - 233  parse函数解析后 => [6,7] - [2,3,3],同样为了使 6,7对应十位,个位
  31.     // 在前面补 0 ,即 [0,6,7] - [2,3,3]
  32.     if (a = xe - ye) {
  33.       if (xlty = a < 0) {
  34.         a = -a;
  35.         t = xc;
  36.       } else {
  37.         ye = xe;
  38.         t = yc;
  39.       }
  40.       t.reverse();
  41.       for (b = a; b--;) t.push(0);  // 补零
  42.       t.reverse();
  43.     } else {
  44.       // 若指数幂相等,不需要补零,则比较两数大小,从最大位开始比较;
  45.       // 比如 [2,3,4] 和 [1,2,3] 最大位是百位,若百位的数字不相等,则可得出孰大孰小
  46.       j = ((xlty = xc.length < yc.length) ? xc : yc).length;
  47.       for (a = b = 0; b < j; b++) {
  48.         if (xc[b] != yc[b]) {
  49.           xlty = xc[b] < yc[b];
  50.           break;
  51.         }
  52.       }
  53.     }
  54.    
  55.     // 对于被减数 x 和减数 y
  56.     // 如果 x - y < 0,则交换两数,并改变符号;比如 2 - 4 = -(4-2)
  57.     if (xlty) {
  58.       t = xc;
  59.       xc = yc;
  60.       yc = t;
  61.       y.s = -y.s;
  62.     }
  63.     // 如果被减数的数字数组长度小于减数,则给被减数的末尾添加 0
  64.     // 比如 12 - 0.0009  parse函数解析后 => [1,2] - [0,0,0,0,9]
  65.     // 因为 9 是小数后几位,相应的需要给 [1,2]末尾补 0 ,即 [1,2,0,0,0] - [0,0,0,0,9]
  66.     if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0;
  67.     // 从 xc 中减去 yc
  68.     for (b = i; j > a;) {
  69.       if (xc[--j] < yc[j]) {
  70.         for (i = j; i && !xc[--i];) xc[i] = 9;
  71.         --xc[i];
  72.         xc[j] += 10;
  73.       }
  74.       xc[j] -= yc[j];
  75.     }
  76.     // 去掉运算结果末尾 0
  77.     for (; xc[--b] === 0;) xc.pop();
  78.     // 去掉运算结果前置 0 ,并减去相应指数幂
  79.     for (; xc[0] === 0;) {
  80.       xc.shift();
  81.       --ye;
  82.     }
  83.     // 运算结果为 0 的情况
  84.     if (!xc[0]) {
  85.       // n - n = +0
  86.       y.s = 1;
  87.       xc = [ye = 0];
  88.     }
  89.     y.c = xc;
  90.     y.e = ye;
  91.     return y;
  92.   };
复制代码
乘法源码的主要逻辑是下面这一段
  1. // 从 被减数 xc 中减去减数 yc
  2. // a 是 xc 和 yc 的幂的差值,j 是 yc 的长度,这里循环条件用 j > a,表示循环 j-a 次
  3. // 比如 120 - 9  =>  [1,2,0]-[0,0,9] 指数幂差是 2 ,减数数字数组长度是 3 ,则只需要循环 3-2=1 次
  4. // 比如 120 - 0.009 => [1,2,0,0,0,0]-[0,0,0,0,0,9] 指数幂差是 5 ,减数数字数组长度是 6 ,则只需要循环 6-5=1 次
  5. for (b = i; j > a;) {
  6.   if (xc[--j] < yc[j]) {
  7.   //从后往前遍历xc,当碰到值为0 ,将值改为 9;
  8.   //比如 [1,0,0]-[0,0,9] => [0,9,10] -
  9.     for (i = j; i && !xc[--i];) xc[i] = 9;
  10.     --xc[i];
  11.     xc[j] += 10;
  12.   }
  13.   xc[j] -= yc[j];
  14. }
复制代码
描述的其实就是以前老师教我们在纸上乘法运算的过程:

以 123*12 来举例子分析上面这段代码

  • xc 是乘数 [1,2,3],yc 是被乘数 [1,2],b 是 yc 长度,a 是 xc 长度
  • c 是保存结果的数组,定义的长度是 a+b
两个数相乘得到的结果长度可能是 a+b,也有可能是 a+b-1。所以后面需要删除数组头部的 0

  • for (i = b; i--;) 首先是外层循环,从数组长度较短的被乘数开始循环,将 b 赋值给 i,i 充当 yc 的长度,而 b 用来保存进位的值
  • b = 0 定义进位的值
  • for (j = a + i; j > i;)  内层乘数(123)的循环,这里的 j 表示在结果数组 c 中的位置
for (j = a + i; j > i;)  实际上就是 for ( j = 乘数长度 + 当前被乘数数字的位置 ),这里是因为当第二轮外层循环时,123 * 1 的时候,1 是 12 的 十位,所以在 j 也应该从十位开始保存计算结果。
第一轮外层循环

第二轮外层循环


  • b = c[j] + yc * xc[j - i - 1] + b 当前数字位置的总和,加上进位


  • c[j--] = b % 10;  当前位置去整取余
  • b = b / 10 | 0;  进位值取整
至此乘法运算逻辑结束
除法

源码
  1. for (i = j; i && !xc[--i];) xc[i] = 9;
复制代码
在除法运算中,对于 a/b , a 是被除数,b 是除数,下面依次分析上面代码

  • if (dp !== ~~dp || dp < 0 || dp > MAX_DP)  判断 dp 是不是大于 0 的整数,并且小于 MAX_DP,这里的 dp 可以自己设置
  1. P.times = P.mul = function (y) {
  2.     var c,
  3.       x = this,
  4.       Big = x.constructor,
  5.       xc = x.c,
  6.       yc = (y = new Big(y)).c,
  7.       a = xc.length,
  8.       b = yc.length,
  9.       i = x.e,
  10.       j = y.e;
  11.     // 确定结果的符号
  12.     y.s = x.s == y.s ? 1 : -1;
  13.     // 其中一个为 0 ,返回结果为 0
  14.     if (!xc[0] || !yc[0]) {
  15.       y.c = [y.e = 0];
  16.       return y;
  17.     }
  18.     // 初始化结果的指数
  19.     y.e = i + j;
  20.     // 对比 xc,yc 长度,xc 存放长度更长的一方
  21.     if (a < b) {
  22.       c = xc;
  23.       xc = yc;
  24.       yc = c;
  25.       j = a;
  26.       a = b;
  27.       b = j;
  28.     }
  29.     // 用 0 初始化结果数组
  30.     for (c = new Array(j = a + b); j--;) c[j] = 0;
  31.     // i is initially xc.length.
  32.     for (i = b; i--;) {
  33.       b = 0;
  34.       // a is yc.length.
  35.       for (j = a + i; j > i;) {
  36.         // Current sum of products at this digit position, plus carry.
  37.         b = c[j] + yc[i] * xc[j - i - 1] + b;
  38.         c[j--] = b % 10;
  39.         // carry
  40.         b = b / 10 | 0;
  41.       }
  42.       c[j] = b;
  43.     }
  44.     // 如果有最终进位,则增加结果的指数,否则删除头部的 0
  45.     if (b) ++y.e;
  46.     else c.shift();
  47.     // 删除尾部的 0
  48.     for (i = c.length; !c[--i];) c.pop();
  49.     y.c = c;
  50.     return y;
  51.   };
复制代码

  • 除数为 0 则抛出错误。
  1. for (i = b; i--;) {
  2.   b = 0;
  3.   for (j = a + i; j > i;) {
  4.     // 当前数字位置的总和,加上进位
  5.     b = c[j] + yc[i] * xc[j - i - 1] + b;
  6.     c[j--] = b % 10;
  7.     // 进位值
  8.     b = b / 10 | 0;
  9.   }
  10.   c[j] = b;
  11. }
复制代码

  • 被除数是 0 则返回值为 0 的实例对象
  1. P.div = function (y) {
  2.     var x = this,
  3.       Big = x.constructor,
  4.       a = x.c,                  // dividend
  5.       b = (y = new Big(y)).c,   // divisor
  6.       k = x.s == y.s ? 1 : -1,
  7.       dp = Big.DP;
  8.     if (dp !== ~~dp || dp < 0 || dp > MAX_DP) {
  9.       throw Error(INVALID_DP);
  10.     }
  11.     // Divisor is zero?
  12.     if (!b[0]) {
  13.       throw Error(DIV_BY_ZERO);
  14.     }
  15.     // Dividend is 0? Return +-0.
  16.     if (!a[0]) {
  17.       y.s = k;
  18.       y.c = [y.e = 0];
  19.       return y;
  20.     }
  21.     var bl, bt, n, cmp, ri,
  22.       bz = b.slice(),
  23.       ai = bl = b.length,
  24.       al = a.length,
  25.       r = a.slice(0, bl),   // remainder
  26.       rl = r.length,
  27.       q = y,                // quotient
  28.       qc = q.c = [],
  29.       qi = 0,
  30.       p = dp + (q.e = x.e - y.e) + 1;    // precision of the result
  31.     q.s = k;
  32.     k = p < 0 ? 0 : p;
  33.     // Create version of divisor with leading zero.
  34.     bz.unshift(0);
  35.     // Add zeros to make remainder as long as divisor.
  36.     for (; rl++ < bl;) r.push(0);
  37.     do {
  38.       // n is how many times the divisor goes into current remainder.
  39.       for (n = 0; n < 10; n++) {
  40.         // Compare divisor and remainder.
  41.         if (bl != (rl = r.length)) {
  42.           cmp = bl > rl ? 1 : -1;
  43.         } else {
  44.           for (ri = -1, cmp = 0; ++ri < bl;) {
  45.             if (b[ri] != r[ri]) {
  46.               cmp = b[ri] > r[ri] ? 1 : -1;
  47.               break;
  48.             }
  49.           }
  50.         }
  51.         // If divisor < remainder, subtract divisor from remainder.
  52.         if (cmp < 0) {
  53.           // Remainder can't be more than 1 digit longer than divisor.
  54.           // Equalise lengths using divisor with extra leading zero?
  55.           for (bt = rl == bl ? b : bz; rl;) {
  56.             if (r[--rl] < bt[rl]) {
  57.               ri = rl;
  58.               for (; ri && !r[--ri];) r[ri] = 9;
  59.               --r[ri];
  60.               r[rl] += 10;
  61.             }
  62.             r[rl] -= bt[rl];
  63.           }
  64.           for (; !r[0];) r.shift();
  65.         } else {
  66.           break;
  67.         }
  68.       }
  69.       // Add the digit n to the result array.
  70.       qc[qi++] = cmp ? n : ++n;
  71.       // Update the remainder.
  72.       if (r[0] && cmp) r[rl] = a[ai] || 0;
  73.       else r = [a[ai]];
  74.     } while ((ai++ < al || r[0] !== UNDEFINED) && k--);
  75.     // Leading zero? Do not remove if result is simply zero (qi == 1).
  76.     if (!qc[0] && qi != 1) {
  77.       // There can't be more than one zero.
  78.       qc.shift();
  79.       q.e--;
  80.       p--;
  81.     }
  82.     // Round?
  83.     if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);
  84.     return q;
  85.   };
复制代码

  • 接下来是除法运算逻辑,定义变量的那一段不贴了,直接看 do while 循环
  1. Big.DP = 30
复制代码
这个循环做了这些事情:以 1234 / 9 为例;


  • 将当前位置的余数 1 和除数 9 比较大小,(一开始余数取的是被除数的前面 n 位,n 和除数的长度大小相同,所以取的是 1 )。先比较长度,长度相同再比较大小。
  • 若除数大于当前位置余数,则跳出循环(当前循环次数即为当前位置的商,9 > 1 ,那么当前循环次数为 0 ,即当前位置商为 0 )
  • 然后保存商 qc[qi++] = cmp ? n : ++n;
  • 最后更新当前余数,除数大于余数时,则当前余数向后借一位,余数就由 1 变为了 12
  1. if (!b[0]) {
  2.   throw Error(DIV_BY_ZERO);
  3. }
复制代码

  • 若除数小于当前余数,则继续从余数中减去除数 (开始减法 for 循环)


  • 然后再次保存商 qc[qi++] = cmp ? n : ++n;
  • 然后再次更新当前余数,除数大于余数时,则当前余数向后借一位,余数就由 3 变为了 33
  • 当商数组的长度没有达到指定的精度总和,继续上面的步骤,直至循环结束;
指定的精度总和指的是 Big.DP(默认20) + (被除数的指数-除数的指数); 1234 / 9 的指定精度总和是 23。

  • 商数组长度大于 1 的情况下,删除数组前面的 0 ;如果是 商就是 0 ,比如 0/1 = 0,这种情况不必删除 0 了
  1. if (!a[0]) {
  2.   y.s = k;
  3.   y.c = [y.e = 0];
  4.   return y;
  5. }
复制代码

  • 舍入操作 if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);
至此除法逻辑结束
注意事项

big.js 用数组存储值,类似 高精度计算,只不过 big.js 是数组中每个位置存储一个值,然后对每个位置进行运算;而对超级大的数字(数百或数千位数值时),big.js 算术运算不如 bignumber.js 快。
例如,bignumber.js 将数字1234.56789的数字存储为[1234,56789000000000] ,即以两个1e14为基数的数组形式存储,而 big.js 存储的数字与[1,2,3,4.5,6,7,8,9]相同,即以9个10为基数的数组形式存储。前者的算术运算可能更快,因为需要处理的元素较少。在实践中,这可能只有在使用数百或数千位数值时才会有所不同。
在使用 big.js 进行运算时需要注意有时候没有设置足够大的精度,会导致结果不是想要的。
  1. Big.DP = 20+Big(1).div('11111111').times('11111111') // 0.9999999999999999// 0.9999999999999999 在 Number 编码的可以表示的准确精度范围内P.times = P.mul = function (y) {
  2.     var c,
  3.       x = this,
  4.       Big = x.constructor,
  5.       xc = x.c,
  6.       yc = (y = new Big(y)).c,
  7.       a = xc.length,
  8.       b = yc.length,
  9.       i = x.e,
  10.       j = y.e;
  11.     // 确定结果的符号
  12.     y.s = x.s == y.s ? 1 : -1;
  13.     // 其中一个为 0 ,返回结果为 0
  14.     if (!xc[0] || !yc[0]) {
  15.       y.c = [y.e = 0];
  16.       return y;
  17.     }
  18.     // 初始化结果的指数
  19.     y.e = i + j;
  20.     // 对比 xc,yc 长度,xc 存放长度更长的一方
  21.     if (a < b) {
  22.       c = xc;
  23.       xc = yc;
  24.       yc = c;
  25.       j = a;
  26.       a = b;
  27.       b = j;
  28.     }
  29.     // 用 0 初始化结果数组
  30.     for (c = new Array(j = a + b); j--;) c[j] = 0;
  31.     // i is initially xc.length.
  32.     for (i = b; i--;) {
  33.       b = 0;
  34.       // a is yc.length.
  35.       for (j = a + i; j > i;) {
  36.         // Current sum of products at this digit position, plus carry.
  37.         b = c[j] + yc[i] * xc[j - i - 1] + b;
  38.         c[j--] = b % 10;
  39.         // carry
  40.         b = b / 10 | 0;
  41.       }
  42.       c[j] = b;
  43.     }
  44.     // 如果有最终进位,则增加结果的指数,否则删除头部的 0
  45.     if (b) ++y.e;
  46.     else c.shift();
  47.     // 删除尾部的 0
  48.     for (i = c.length; !c[--i];) c.pop();
  49.     y.c = c;
  50.     return y;
  51.   };+Big(1).div('11111111').times('11111111') // 1// 而设置 P.times = P.mul = function (y) {
  52.     var c,
  53.       x = this,
  54.       Big = x.constructor,
  55.       xc = x.c,
  56.       yc = (y = new Big(y)).c,
  57.       a = xc.length,
  58.       b = yc.length,
  59.       i = x.e,
  60.       j = y.e;
  61.     // 确定结果的符号
  62.     y.s = x.s == y.s ? 1 : -1;
  63.     // 其中一个为 0 ,返回结果为 0
  64.     if (!xc[0] || !yc[0]) {
  65.       y.c = [y.e = 0];
  66.       return y;
  67.     }
  68.     // 初始化结果的指数
  69.     y.e = i + j;
  70.     // 对比 xc,yc 长度,xc 存放长度更长的一方
  71.     if (a < b) {
  72.       c = xc;
  73.       xc = yc;
  74.       yc = c;
  75.       j = a;
  76.       a = b;
  77.       b = j;
  78.     }
  79.     // 用 0 初始化结果数组
  80.     for (c = new Array(j = a + b); j--;) c[j] = 0;
  81.     // i is initially xc.length.
  82.     for (i = b; i--;) {
  83.       b = 0;
  84.       // a is yc.length.
  85.       for (j = a + i; j > i;) {
  86.         // Current sum of products at this digit position, plus carry.
  87.         b = c[j] + yc[i] * xc[j - i - 1] + b;
  88.         c[j--] = b % 10;
  89.         // carry
  90.         b = b / 10 | 0;
  91.       }
  92.       c[j] = b;
  93.     }
  94.     // 如果有最终进位,则增加结果的指数,否则删除头部的 0
  95.     if (b) ++y.e;
  96.     else c.shift();
  97.     // 删除尾部的 0
  98.     for (i = c.length; !c[--i];) c.pop();
  99.     y.c = c;
  100.     return y;
  101.   }; 后//结果数组保存的是 999999999999999999999999//超过了 Number 编码的可以表示的准确精度范围,则会舍入为 1
复制代码
总结

本文剖析了 big.js 解析函数源码,四则运算源码,分别用图文详细描述了运算过程,一步步还原了作者的构思。有不正确的地方或者不同见解还请各位大佬提出来。

来源:https://www.cnblogs.com/zsxblog/p/17864751.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具