JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

3.3 数值的扩展与修复

数值没有什么好扩展的,而且JavaScript的数值精度问题一向臭名昭著,要修复它们可不是一两行代码了事。先看扩展,我们只把目光集中于Prototype.js与mootools就行了。

Prototype.js 为它添加 8 个原型方法。Succ 是加 1,times 是将回调重复执行指定次数toPaddingString与上面提到字符串扩展方法pad作用一样,toColorPart是转十六进制,abs、ceil、floor、abs是从Math中偷来的。

mootools 的情况:limit 是从数值限定在一个闭开间中,如果大于或小于其边界,则等于其最大值或最小值,times与Prototype.js的用法相似, round是Math.round的增强版,添加了精度控制, toFloat、toInt是从window中偷来的,其他的则是从Math中偷来的。

在es5_shim.js这个库,它实现了ECMA262v5提到的一个内部方法toInteger。

      // http://es5.github.com/#x9.4
      // http://jsperf.com/to-integer
      var toInteger = function(n) {
          n = +n;
          if (n !== n) { // isNaN
              n = 0;
          } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
              n = (n > 0 || -1) * Math.floor(Math.abs(n));
          }
          return n;
      };

但依我看来都没什么意义,数值往往来自用户输入,我们一个正则就能判定它是不是一个“数”,是则直接Number(n)!

基于同样的理由,mass Framework对数字的扩展也是很少的,三个独立的扩展。

limit 方法:确保数值在[n1,n2]闭区间之内,如果超出限界,则置换为离它最近的最大值或最小值。

      function limit(target, n1, n2) {
          var a = [n1, n2].sort();
          if (target < a[0])
              target = a[0];
          if (target > a[1])
              target = a[1];
          return target;
      }

nearer方法:求出距离指定数值最近的那个数。

      function nearer(target, n1, n2) {
          var diff1 = Math.abs(target - n1),
                  diff2 = Math.abs(target - n2);
          return diff1 < diff2 ? n1 : n2
      }

Number下唯一需要修复的方法是toFixed,它是用于校正精确度,最后的那个数会做四舍五入操作,但在一些浏览器中并没有这样干。

想简单修复可以这样处理:

      if (0.9.toFixed(0) !== '1') {
          Number.prototype.toFixed = function(n) {
              var power = Math.pow(10, n);
              var fixed = (Math.round(this * power) / power).toString();
              if (n == 0)
                  return fixed;
              if (fixed.indexOf('.') < 0)
                  fixed += '.';
              var padding = n + 1 - (fixed.length - fixed.indexOf('.'));
              for (var i = 0; i < padding; i++)
                  fixed += '0';
              return fixed;
          };
      }

追求完美的话,还存在这样一个版本,把里面的加、减、乘、除都重新实现了一遍。

      if (!Number.prototype.toFixed || (0.00008).toFixed(3) !== '0.000' ||
              (0.9).toFixed(0) === '0' || (1.255).toFixed(2) !== '1.25' ||
              (1000000000000000128).toFixed(0) !== "1000000000000000128") {
          // 一些内部方法与变量,防止全局污染
          (function() {
              var base, size, data, i;
              base = 1e7;
              size = 6;
              data = [0, 0, 0, 0, 0, 0];
              function multiply(n, c) {
                  var i = -1;
                  while (++i < size) {
                      c += n * data[i];
                      data[i] = c % base;
                      c = Math.floor(c / base);
                  }
              }
              function divide(n) {
                  var i = size, c = 0;
                  while (--i >= 0) {
                      c += data[i];
                      data[i] = Math.floor(c / n);
                      c = (c % n) * base;
                  }
              }
              function toString() {
                  var i = size;
                  var s = '';
                  while (--i >= 0) {
                      if (s !== '' || i === 0 || data[i] !== 0) {
                          var t = String(data[i]);
                          if (s === '') {
                              s = t;
                          } else {
                              s += '0000000'.slice(0, 7 - t.length) + t;
                          }
                      }
                  }
                  return s;
              }
              function pow(x, n, acc) {
                  return (n === 0 ? acc : (n % 2 === 1 ? pow(x, n - 1, acc * x)
                          : pow(x * x, n / 2, acc)));
              }
              function log(x) {
                  var n = 0;
                  while (x >= 4096) {
                      n += 12;
                      x /= 4096;
                  }
                  while (x >= 2) {
                      n += 1;
                      x /= 2;
                  }
                  return n;
              }
              Number.prototype.toFixed = function(fractionDigits) {
                  var f, x, s, m, e, z, j, k;
                  // Test for NaN and round fractionDigits down
                  f = Number(fractionDigits);
                  f = f !== f ? 0 : Math.floor(f);
                  if (f < 0 || f > 20) {
                      throw new RangeError("Number.toFixed called with invalid number of decimals");
                  }
                  x = Number(this);
                  // Test for NaN
                  if (x !== x) {
                      return "NaN";
                  }
                  // If it is too big or small,return the string value of the number
                  if (x <= -1e21 || x >= 1e21) {
                      return String(x);
                  }
                  s = "";
                  if (x < 0) {
                      s = "-";
                      x = -x;
                  }
                  m = "0";
                  if (x > 1e-21) {
                      // 1e-21 < x < 1e21
                      // -70 < log2(x) < 70
                      e = log(x * pow(2, 69, 1)) - 69;
                      z = (e < 0 ? x * pow(2, -e, 1) : x / pow(2, e, 1));
                      z *= 0x10000000000000; // Math.pow(2, 52);
                      e = 52 - e;
                      // -18 < e < 122
                      // x = z / 2 ^ e
                      if (e > 0) {
                          multiply(0, z);
                          j = f;
                          while (j >= 7) {
                              multiply(1e7, 0);
                              j -= 7;
                          }
                          multiply(pow(10, j, 1), 0);
                          j = e - 1;
                          while (j >= 23) {
                              divide(1 << 23);
                              j -= 23;
                          }
                          divide(1 << j);
                          multiply(1, 1);
                          divide(2);
                          m = toString();
                      } else {
                          multiply(0, z);
                          multiply(1 << (-e), 0);
                          m = toString() + '0.00000000000000000000'.slice(2, 2 + f);
                      }
                  }
                  if (f > 0) {
                      k = m.length;
                      if (k <= f) {
                          m = s + '0.0000000000000000000'.slice(0, f - k + 2) + m;
                      } else {
                          m = s + m.slice(0, k - f) + '.' + m.slice(k - f);
                      }
                  } else {
                      m = s + m;
                  }
                  return m;
              }
          }());
      }

toFixed 方法实现得如此艰难其实也不能怪浏览器,计算机所理解的数字与我们是不一样的。众所周知,计算机的世界是2进制,数字也不例外。为了储存更复杂的结构,需要用到更高维的进制。而进制间的换算是存在误差的。虽然计算机在一定程度上反映了现实世界,但它提供的顶多只是一个“幻影”,经常与我们的常识产生偏差。例如,将1除以3,然后再乘以3,最后得到的值竟然不是1;10个0.1相加也不等于1;交换相加的几个数的顺序,却得到了不同的和http://www.html-js.com/article/1630

      console.log(0.1 + 0.2)
      console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true
      console.log(Infinity > 100) //true
      console.log(JSON.stringify(25001509088465005)) //25001509088465004
      console.log(0.1000000000000000000000000001) //0.1
      console.log(0.100000000000000000000000001) //0.1
      console.log(0.1000000000000000000000000456) //0.1
      console.log(0.09999999999999999999999) //0.1
      console.log(1 / 3) //0.3333333333333333
      console.log(23.53 + 5.88 + 17.64)// 47.05
      console.log(23.53 + 17.64 + 5.88)// 47.050000000000004

这些其实不是BUG,而是我们无法接受这事实。在JavaScript中,数值有三种保存方式。

· 字符串形式的数值内容。

· IEEE754标准双精度浮点数,它最多支持小数点后带15~17位小数,由于存在2进制和10进制的转换问题,具体的位数会发生变化。

· 一种类似于C语言的init类型的32位整数,它由4个8 bit的字节构成,可以保存较小的整数。

当JavaScript遇到一个数值时,它会首先尝试按照整数来处理该数值,如行得通,则把数值保存为31 bit的整数;如果该数值不能视为整数,或超出31 bit的范围,则把数值保存为64位的IEEE 754浮点数。

聪明的读者一定想到了这样一个问题:什么时候规规矩矩的整数会突然变成捉摸不定的双精度浮点数?答案是:当它们的值变得非常庞大时,或者进入1和0之间时。因此,1和0是首先必须注意的两个数值。

接下来,最大的Unicode值是1114111(7位数字,相当于(/x41777777),而最大的RGB颜色值是16777215(8位数字,相当于#FFFFFF)。最大的32 bit带符号整数是2147483647(10位数字,即Math.pow(2,31)-1),-2147483648最小,所以JavaScript内部会以整数的形式保存所有Unicode值和 RGB 颜色。2147483647 是第三个必须注意的数值,任何大于该值的数据将保存为双精度格式。

9007199254740992(16位数字,即Math.pow(2,53))是最大的浮点数,输出时类似整数,所有Date对象(按毫秒计算)都小于该值,因此总是模拟整数的格式输出。它是第四个必须注意的数值。

最后,最大的双精度数值是1.7976931348623157e+308,超出这个范围就要算作无穷大了。

因此,我们就看出缘由了,大数相加出问题是由于精度的不足,小数相加出问题是进制转算时产生误差。第一个好理解。第二个,主要是我们常用的10进制转换为2进制时,变成循环小数及无理数等有无限多小数位的数,计算机要用位数有限的浮点数来表示是无法实现的,只能从某一位进行截短。而且,因为内部表示是2进制,10进制看起来是能除尽的数,往往在2进制是循环小数。比如用2进制来表示10进制的0.1,就得写成2的幂(因为小于1,所以幂是负数)相加的形式。若一直持续下去,0.1就成了0.000110011001100110011...这种循环小数。在有效数字的范围内进行舍入,就会产生误差。

综上所述,我们就尽量避免小数操作与大数操作,或者转交后台去处理,实在避免不了就引入专业的库来处理。