今天看啥  ›  专栏  ›  西风瘦码

JS处理大数相加问题

西风瘦码  · 掘金  · 前端  · 2019-06-25 06:29
阅读 24

JS处理大数相加问题

先从一个例子讲起

var number1 = 10000000000000000000000000 + 11111111111111111111111111   //理论上number1的值应该是21111111111111111111111111(javascript中会表示为科学计数法:2.111111111111111e+25)
var number2 = 21111111111111111111111000
console.log(number1 === number2)  //true
复制代码

这个不用算简单看一下都知道计算结果不对,最后几位肯定应该都是1的,可是为什么会得到一个不对的值呢?

JavaScript Number的精度丢失问题

因为JavaScriptNumber类型是遵循IEEE 754规范表示的,这就意味着JavaScript能精确表示的数字是有限的,JavaScript可以精确到个位的最大整数是9007199254740992,也就是2的53次方,超过这个范围就会精度丢失,造成JavaScript无法判断大小,从而会出现下面的现象:

Math.pow(2, 53);    // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1;    // true
9007199254740992 === 9007199254740992 + 1;    // true
复制代码

可以从下面这张图上看到JavaScript Number能够精确表示的上下限:

image

解决方案

那当两个数据相加时,其中一个或者两个数据都超过了这个精度范围,直接相加结果就会不准了,那怎么解决呢?

参考网上常用的一中方案是将Number转为String,然后将String转为Array,并且注意补齐较短的数组,将他们的长度标称一样再一一相加得到一个新数组,再讲和组成的新数组转为数字就可以了,下面是实现代码:

function sumString(a, b) {
  a = '0' + a;
  b = '0' + b;  //加'0'首先是为了转为字符串,而且两个数相加后可能需要进位,这样保证了和的长度就是a、b中长的那个字符的长度
  var arrA = a.split(''),  //将字符串转为数组
      arrB = b.split(''),
      res = [],  //相加结果组成的数组
      temp = '',  //相同位数相加的值
      carry = 0,  //同位数相加结果大于等于10时为1,否则为0
      distance = a.length - b.length,  //计算两个数字字符串的长度差
      len = distance > 0 ? a.length : b.length;  //和的长度
  // 在长度小的那个值前加distance个0,保证两个数相加之前长度是想等的
  if(distance > 0) {
    for(let i = 0; i < distance; i++) {
      arrB.unShift('0');
    }
  }else{
    for(let i = 0; i < distance; i++) {
      arrA.unShift('0');
    }
  }
  // 现在得到了两个长度一致的数组,需要做的就是把他们想通位数的值相加,大于等于10的要进一
  // 最终得到一个和组成的数组,将数组转为字符串,去掉前面多余的0就得到了最终的和
  for(let i = len-1; i >= 0; i--) {
    temp = Number(arrA[i]) + Number(arrB[i]) + carry;
    if(temp >= 10) {
      carry = 1;
      res.unshift((temp + '')[1])
    }
    else{
      carry = 0;
      res.unshift(temp)
    }
  }
  res = res.join('').replace(/^0/, '');
  console.log(res);
}
复制代码

关于0.1+0.2 !== 0.3的问题

前面讲到,在JavaScript中,使用浮点数标准IEEE 754表示数字的,在表示小数的时候,在转化二进制的时候有些数是不能完整转化的,比如0.3,转化成二进制是一个很长的循环的数,是超过了JavaScript能表示的范围的,所以近似等于0.30000000000000004。

这个是二进制浮点数最大的问题(不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此)。

怎么判断两个值是否想等

在这里我们要引入ES6中在Number对象上新增的一个极小的常量Number.EPSILON。它表示1与大于1的最小浮点数之差,等于2的-52次方。

Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

所以可以用这个来判断两个数浮点数是否想等:

function numIsEqual(lef, rig) {
    return Math.abs(lef - rig) < Number.EPSILON
}
复制代码

如果考虑浏览器兼容的话可以这样写:

function numIsEqual(lef, rig) {
    let EPSILON = Number.EPSILON ? Number.EPSILON : Math.pow(2,-52)
    return Math.abs(lef - rig) < EPSILON
}
复制代码

当然我们日常使用小数的话一般精确到小数点后两位就够了,是不会有太大的问题的,但是一定要记得用toFied()方法保留小数点后位数,不能直接取计算的值,避免误差。

尾巴

关于JavaScript中数据的表示问题,我之前在极客时间的《深入浅出计算机组成原理》中还听到过一些,那里面详细讲解了为什么浮点数的IEEE 754标准只能表示有限的数字,不过对于这么底层的东西我还只能勉强意会,哈哈。

参考




原文地址:访问原文地址
快照地址: 访问文章快照