Skip to content

JavaScript中精度丢失的处理

前言

JavaScript 中,可以使用 toFixed() 方法来限制浮点数的小数位数,但是这种方式只能限制小数点后面的位数,并不能解决精度丢失的问题。比较好的解决方案是使用第三方库,如 BigNumber.jsdecimal.js,这些库提供了高精度计算的支持,可以避免精度丢失的问题。

BigNumber.js的原理

最小化实现

以下是一个简单的实现,使用一个数组来存储大数,每个元素都是一个 10 进制数,从而避免了数字超出范围的问题:

ts
function BigNumber(num) {
  this.parts = [];
  num = num.toString();
  for (var i = num.length - 1; i >= 0; i -= 9) {
    var part = num.slice(Math.max(i - 8, 0), i + 1);
    this.parts.push(parseInt(part, 10));
  }
}

BigNumber.prototype.toString = function () {
  var str = "";
  for (var i = this.parts.length - 1; i >= 0; i--) {
    str += this.parts[i].toString();
  }
  return str.replace(/^0+/, "");
};

BigNumber.prototype.plus = function (other) {
  var result = new BigNumber(0);
  var carry = 0;
  for (var i = 0; i < Math.max(this.parts.length, other.parts.length); i++) {
    var x = this.parts[i] || 0;
    var y = other.parts[i] || 0;
    var sum = x + y + carry;
    result.parts.push(sum % 1000000000);
    carry = Math.floor(sum / 1000000000);
  }
  if (carry) {
    result.parts.push(carry);
  }
  return result;
};

BigNumber.prototype.times = function (other) {
  var result = new BigNumber(0);
  var carry = 0;
  for (var i = 0; i < this.parts.length; i++) {
    for (var j = 0; j < other.parts.length; j++) {
      var prod = this.parts[i] * other.parts[j] + carry;
      var k = i + j;
      while (prod > 0) {
        if (k >= result.parts.length) {
          result.parts.push(0);
        }
        prod += result.parts[k];
        result.parts[k] = prod % 1000000000;
        prod = Math.floor(prod / 1000000000);
        k++;
      }
      carry = 0;
    }
  }
  return result;
};

这个实现将大数分成了每个 9 位一组,每个元素都是一个 10 进制数,使用数组来存储每个组,这样就避免了数字超出范围的问题。同时,该实现还支持加法和乘法运算,其他运算也可以类似实现。

使用

ts
var a = new BigNumber("12345678901234567890");
var b = new BigNumber("98765432109876543210");

console.log(a.plus(b).toString()); // "111111111111111111100"
console.log(a.times(b).toString()); // "12193263113702179522395810392542705102900"

decimal.js的原理

最小化实现

decimal.js 的实现原理是基于 BigInt,这是一个 ES2020 引入的内置类型,用于处理超大整数。因此,decimal.js 可以直接利用 BigInt 实现数字的精确计算,从而避免了精度丢失的问题。

ts
class Decimal {
  constructor(value) {
    this.value = BigInt(value);
  }

  plus(other) {
    return new Decimal(this.value + BigInt(other));
  }

  toString() {
    return this.value.toString();
  }
}

这里定义了一个 Decimal 类,它包含一个 value 属性,用来存储 BigInt 类型的数值。类中定义了两个方法:plus 用于执行加法运算,返回一个新的 Decimal 实例;toString 用于将 Decimal 转换为字符串表示。

使用

ts
var a = new Decimal("12345678901234567890");
var b = new Decimal("98765432109876543210");

console.log(a.plus(b).toString()); // "111111111111111111100"

实现差异

虽然 BigNumber.jsdecimal.js 都是用来解决数字精度问题的库,但它们的实现原理不同。

decimal.js 的实现原理是基于 BigInt,这是一个 ES2020 引入的内置类型,用于处理超大整数。因此,decimal.js 可以直接利用 BigInt 实现数字的精确计算,从而避免了精度丢失的问题。

相比之下,BigNumber.js 的实现原理则是将数字拆分成多个部分存储,每个部分都是一个 10 进制数。这种数据结构的优点是可以处理超过 Number 类型表示范围的数字,但它的性能可能不如直接使用 BigInt

在使用这两个库时,需要根据具体情况选择。如果只需要处理整数,且运行环境支持 BigInt,则可以考虑使用 decimal.js。如果需要处理超过 Number 范围的数字,或者运行环境不支持 BigInt,则可以使用 BigNumber.js。

参考