前端大数精度处理方案

发布时间 2023-08-28 11:20:24作者: 雾夜飞雨

问题

在我们常见的 JavaScript 数字运算中,小数和大数都是会让我们比较头疼的两个数据类型。

  • 在大数运算中,由于 number 类型的数字长度限制,我们经常会遇到超出范围的情况。比如:后端给前端返回一个数字类型的 id,但是前端对这个 id 不做任何处理,直接使用到下一个给后端请求的时候,接口报错了,后端一查,说你前端 id 给传错了,然后前端一看果然是给传错了,但是自己又没有做什么,怎么拿的就怎么给后端了。原因就是因为 :
    后端给的数字太大超过 -9007199254740991 (-(2^53-1))到 9007199254740991(2^53-1)之间的整数,前端 js 这块就会自动四舍五入,导致精度丢失。
  • 而在小数点数字进行运算的过程中,JavaScript 又由于它的数据表示方式,从而导致了小数运算会有不准确的情况。最经典的一个例子就是 0.3 - 0.2,并不等于 0.1,而是等于 0.09999999999999998。

究其原因可以参考这篇文章:前端应该知道的JavaScript浮点数和大数的原理

解决方案

BigInt

因为 number 的基本类型不能超过2^53,不然就会精度丢失,为了解决这个限制,在 ECMAScript 标准中出现了BigInt,BigInt可以表示任意大的整数。

创建

  • 直接BigInt去创建;
    BigInt(value)

  • 后面加个 n;

它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

更多使用方式参考 MDN 官方文档:BigInt

第三方库

使用专门处理大数精度的第三方库,如 bignumber.js、big.js 或 decimal.js 等,是一种常见的解决方案。这些库提供了高精度的计算功能,可以避免 JavaScript 原生的浮点数精度问题。

库名称
简介
特征
包大小(压缩后)
适用场景
Math.js

适用于 JavaScript 和 Node.js 的扩展数学库。

它具有灵活的表达式解析器,支持符号计算,附带大量内置函数和常量,并提供集成解决方案来处理不同的数据类型,如数字、大数、复数、分数、单位和矩阵。

功能强大且易于使用。

  • 支持数字、大数、复数、分数、单位、字符串、数组和矩阵
  • 与 JavaScript 的内置数学库兼容
  • 包含灵活的表达式解析器
  • 进行符号计算
  • 带有大量内置函数和常量
  • 也可以用作命令行应用程序
  • 可在任何 JavaScript 引擎上运行
  • 易于扩展
  • 开源
197K 科学计算、统计分析、数据可视化等领域
decimal.js JavaScript 的任意精度 Decimal 类型
  • 整数和浮点数
  • 简单但功能齐全的API
  • Number.prototype 复制 JavaScript 和 Math 对象的许多方法
  • 还处理十六进制、二进制和八进制值
  • 比 Java BigDecimal 的 JavaScript 版本更快、更小,而且可能更容易使用
  • 无依赖关系
  • 广泛的平台兼容性:仅使用 JavaScript 1.5 (ECMAScript 3) 功能
  • 全面的文档和测试集
  • 由 math.js 在幕后使用
  • 包含 TypeScript 声明文件:decimal.d.ts
 32 KB 科学类应用
bignumber.js 用于任意精度算术的 JavaScript 库
  • 整数和小数
  • API简单但功能齐全
  • 比 Java BigDecimal 的 JavaScript 版本更快、更小,而且可能更容易使用
  • 8 KB 缩小并压缩
  • 复制JavaScript Number 类型的 toExponentialtoFixedtoPrecision 和方法 toString
  • 包括一个 toFraction 和一个正确舍入的squareRoot方法
  • 支持加密安全的伪随机数生成
  • 无依赖关系
  • 广泛的平台兼容性:仅使用 JavaScript 1.5 (ECMAScript 3) 功能
  • 全面的文档和测试集
8 KB 金融应用、货币计算等领域
big.js 一个小型、快速、易于使用的库,用于任意精度的十进制算术
  • 简单的API
  • 比 Java BigDecimal 的 JavaScript 版本更快、更小且更易于使用
  • 缩小后仅 6 KB
  • 复制 JavaScript Numbers 的 toExponential,toFixed 和方法 toPrecision
  • 以可访问的十进制浮点格式存储值
  • 全面的文档和测试集
  • 无依赖关系
  • 仅使用 ECMAScript 3,因此适用于所有浏览器
 6 KB 简单计算需求、小型项目等场景

自定义运算函数

自己编写处理大数的函数,以下以加法为例:

 

let a = "9876543210123456789000000000123";
let b = "1234567898765432100000012345678901";
function add(str1, str2) {
  // 获取两个数字的最大长度
  let maxLength = Math.max(str1.length, str2.length);
  // 用0补齐长度,让它们两个长度相同
  str1 = str1.padStart(maxLength, 0); // "0009876543210123456789000000000123"
  str2 = str2.padStart(maxLength, 0); // "1234567898765432100000012345678901"
  let temp = 0; // 每个位置相加之和
  let flag = 0; // 进位:相加之和如果大于等于 10,则需要进位
  let result = "";
  for(let i=maxLength-1; i>=0; i--) {
    // 获取当前位置的相加之和:字符串 1 + 字符串 2 + 进位数字
    temp = parseInt(str1[i]) + parseInt(str2[i]) + flag;
    // 获取下一个进位
    flag = Math.floor(temp/10);
    // 拼接结果字符串
    result = temp%10 + result;
  }
  if(flag === 1) {
    // 如果遍历完成后,flag 还剩 1,说明两数相加之后多了一位,类似于:95 + 10 = 105
    result = "1" + result;
  }
  return result;
}

实现相对比较麻烦,且容易出错,不推荐。

case 实践

以后端返回数字类型的 id 超出 2^53-1 这个 case 为例,采用第三方库 big.js 来处理,封装处理函数如下:

// 引入 big.js库
const Big = require('big.js');
 
function processId(id) {
  let processedId;
 
  if (typeof id === 'string') {
    // 将字符串类型的 id 转换为 Big 对象
    processedId = new Big(id);
  } else if (typeof id === 'number') {
    // 将数字类型的 id 转换为 Big 对象
    processedId = new Big(id.toString());
  } else {
    throw new Error('Invalid id type. Expected string or number.');
  }
 
  // 判断 id 是否超出 JavaScript Number 类型范围
  if (!Big(processedId).eq(id)) {
    // 使用 big.js 库处理超出范围的 id
    processedId = Big(id);
  }
 
  return processedId.toString();
}

上述代码中,我们首先引入了 big.js 库,并定义了一个 processId 函数。该函数接受一个id作为参数,可以是字符串类型或者数字类型。

在函数中,我们首先判断 id 的类型,如果是字符串类型,将其转换为 Big 对象;如果是数字类型,则先转换成字符串再转换为 Big 对象。

然后,我们判断 id 是否超出了JavaScript Number类型的范围,通过使用 Big 对象对比原始 id 和转换后的 processedId 是否相等来判断。如果不相等,表示 id 超出了JavaScript Number 类型的范围,我们使用 Big 对象重新处理该 id。

最后,我们将处理后的id通过 toString() 方法转换为字符串,并返回该处理后的 id。