如何在 Node.js 应用程序中处理大数

如何在 Node.js 应用程序中处理大数

计算机很难在不损失精度的情况下准确地表示具有很多个有效数字的数字。当你将超过 JavaScript 中最大安全整数限制的整数存储为普通整数时,它们会失去精度。

在 JavaScript 生态系统中,你可以使用 BigInt 来处理大整数。但是,你也可以使用具有类似于 BigInt 功能的第三方库。

本文将是使用 BigInt 和提供类似功能的流行库管理大数的完整指南。我们还将比较第三方库的用例、优势和劣势。

JavaScript 如何编码数字?

表示大数时精度损失的挑战并不是 JavaScript 独有的。在内部,JavaScript 使用双精度二进制浮点格式来表示数字。

双精度二进制浮点格式

双精度二进制浮点格式由 IEEE 标准 754 定义。它使用 64 位来表示带符号的浮点数。用双精度二进制浮点数表示的数由符号、尾数和指数三部分组成,如下图所示:

image.png

双精度二进制浮点格式将 64 位分配给这三部分。它使用 1 位编码符号,11 位编码偏置指数,52 位编码尾数或尾数。

下面的示例显示了十进制数 -1.7976931348623157e+308 的内部双精度二进制浮点数表示。我使用了 字符来分隔这三个部分的编码。

第一位编码符号。因为我们正在编码一个负数,所以它的值为一。如果我们编码一个正数,它的值将为零。随后的 11 位编码偏置指数,最后 52 位编码尾数:

1111111111101111111111111111111111111111111111111111111111111111

计算机只能理解二进制。因此,在存储或执行数学运算之前,JavaScript 在内部将每个数字转换为双精度二进制浮点格式,如上例所示。

然而,某些数字无法准确地用二进制表示。因此,当你将某些数字从十进制转换为二进制再转换回十进制时,它们会失去精度。

同样,JavaScript 使用固定位数对双精度二进制浮点数的不同部分进行编码。因此,在处理大整数时,你需要使用第三方库或内置的 bigint 类型。

JavaScript 中的最小和最大安全整数

由于双精度格式将表示尾数的位数限制为 53,因此你可以使用的 JavaScript 整数的精度和准确性存在限制。

你可以在不丢失精度的情况下使用的最大安全整数是 2 ** 53 - 1。它也是使用 Number.MAX_SAFE_INTEGER 访问 Number 构造函数的静态数据属性:

console.log(2 ** 53 - 1 === Number.MAX_SAFE_INTEGER) // true

还有一个对应的最小安全整数,其值为-(2**53-1)。你可以使用 Number.MIN_SAFE_INTEGER 静态属性访问它的值:

console.log(-(2 ** 53 - 1) === Number.MIN_SAFE_INTEGER) // true

你执行的任何涉及大于最大安全整数或小于最小安全整数的整数的数学运算都将导致意外的近似结果:

const maxSafeInteger = Number.MAX_SAFE_INTEGER;
const minSafeInteger = Number.MIN_SAFE_INTEGER;

console.log(maxSafeInteger + 1); // 9007199254740992
console.log(maxSafeInteger + 2); // 9007199254740992
console.log(maxSafeInteger + 1 === maxSafeInteger + 2); // true

console.log(minSafeInteger - 1); // -9007199254740992
console.log(minSafeInteger - 2); // -9007199254740992
console.log(minSafeInteger - 1 === minSafeInteger - 2); // true

JavaScript 中的正无穷大和负无穷大

就像上面的最小和最大安全整数一样,JavaScript 有一个它可以在内部表示的最大数值。该值为 2 ** 2014 - 1。你可以使用 Number.MAX_VALUE 数据属性访问它。

JavaScript 使用 Infinity 表示任何超过 Number.MAX_VALUE 的数值,并使用 -Infinity 表示相应的负值,如下例所示:

console.log(Number.MAX_VALUE * 2); // Infinity
console.log(Number.MAX_VALUE * 3); // Infinity
console.log(-Number.MAX_VALUE * 3); // -Infinity

尽管 Infinity 在 Node 中是全局的,但你可以使用 Number.POSITIVE_INFINITY 数据属性访问它,并使用 Number.NEGATIVE_INFINITY 数据属性访问 -Infinity

如何使用 BigInt 在 JavaScript 中管理大整数

正如前面部分所介绍的那样,JavaScript 在内部使用双精度格式来表示数字。因为它使用 53 位来编码尾数,所以你可以在 JavaScript 中使用的最大安全整数是 2**53 - 1

要安全地处理大于最大安全整数的整数,你需要 bigint 类型。它是在不损失精度的情况下操作大整数的内置功能。

你可以通过将 n 附加到整数或使用 BigInt 函数来创建 bigint 类型。因为 BigInt 不是构造函数,所以不使用 new 关键字调用它,如下面的示例所示:

const number = 1n;
console.log(1n + 2n); // 3n

const maxSafeInt = BigInt(Number.MAX_SAFE_INTEGER);
console.log(maxSafeInt + 1n); // 9007199254740992n
console.log(maxSafeInt + 2n); // 9007199254740993n
console.log(maxSafeInt * maxSafeInt); // 81129638414606663681390495662081n

与普通的数字类型不同,你不能将内置的 Math 方法与 BigInt 值一起使用。但是,你可以使用 bigint 类型执行基本的数学运算,例如加法减法求幂

console.log(2n + 3n) // 5n
console.log(2n - 3n)  // -1n
console.log(2n ** 3n) // 8n
console.log(4n % 3n)  // 1n
console.log(BigInt(3) - 4n) // -1n

由于你只能使用 bigint 类型执行基本的数学运算,因此在 JavaScript 中处理大量数字时,你可能需要为某些用例使用第三方库。

在 JavaScript 中管理大数的库

除了内置的 bigint 类型之外,还有几个第三方库可以在 JavaScript 中处理大数字。其中一些库附带了 BigInt 可能不提供的解决方案。

但是,与任何第三方库一样,使用它们也有缺点。它们伴随着额外的打包大小、维护、安全和许可问题。

使用 Math.js 管理大数

Math.js 是一个免费、开源且功能丰富的数学库。你既可以在浏览器中使用它,也可以在 Node 运行时环境中使用它。

虽然它是一个功能丰富的库,但在本文中,我们将使用 Math.js 在 Node 运行时环境中管理大数。用你的包管理器 npm 安装它,如下所示:

# npm 
npm i mathjs

# yarn
yarn add mathjs

#pnpm
pnpm add mathjs

安装 Math.js 后,你可以使用默认配置加载和使用它,如下例所示:

const {
    
     add, subtract, evaluate }= require('mathjs');

const sum = add(2, 3);
const difference = subtract(2, 3);
const anotherSum = evaluate('2 + 3');

console.log(sum); // 5
console.log(difference}); // -1
console.log(anotherSum}); // 5

你可以使用自定义配置创建一个 Math.js 实例,而不是使用默认配置的 Math.js 内置函数:

const {
    
     create, all } = require("mathjs");

const config = {
    
    };
const math = create(all, config);

console.log(math.add(2, 3)); // 5
console.log(math.pow(2, 3)); // 8
console.log(math.divide(4, 2)); // 2
console.log(math.multiply(2, 3)); // 6

Math.js 具有专门用于处理大数的 BigNumber 数据类型。

在上面的一个部分中,我们强调了在使用内置数字类型时,JavaScript 使用 Infinity 表示超过最大可表示数值的数字。

使用 Math.js,你可以表示超过最大可表示数的数字并对它们执行数学运算。但是,请注意,对 BigNumber 类型执行数学运算比对普通数字类型执行数学运算要慢

const {
    
     create, all } = require("mathjs");

const config = {
    
    };
const math = create(all, config);

const maxValue = math.bignumber(Number.MAX_VALUE);
console.log(math.add(maxValue, maxValue)); // 3.5953862697246314e+308

const maxSafeInt = math.bignumber(Number.MAX_SAFE_INTEGER);

console.log(math.square(maxSafeInt)); // 8.1129638414606663681390495662081e+31
console.log(math.add(maxSafeInt, maxSafeInt)); // 18014398509481982
console.log(math.subtract(maxSafeInt, maxSafeInt)); // 0
console.log(math.multiply(maxSafeInt, math.bignumber(2))); // 18014398509481982
console.log(math.divide(maxSafeInt, math.bignumber(2))); // 4503599627370495.5

console.log(math.log10(maxSafeInt)); // 15.95458977019100329811178809273377220616031325194798178472905735
console.log(math.pow(maxSafeInt, math.bignumber(2))); // 8.1129638414606663681390495662081e+31

使用 bignumber.js 管理大数

bignumber.js 是另一个用于管理任意精度十进制和非十进制数的 JavaScript 库。它是一个免费、开源、MIT 许可的库,用于处理大数据。

它可在浏览器、Node 和 Deno 中运行。要开始使用 bignumber.js,请用 npm 安装它:

# npm 
npm i bignumber.js

# yarn
yarn add bignumber.js

#pnpm
pnpm add bignumber.js

安装后,导入并创建一个 BigNumber 构造函数的实例,它以数字、字符串或 BigNumber 类型作为参数并返回一个对象。

在下面的示例中,我使用 commonjs 语法导入 bignumber.js。它还支持 ES 语法。如果你打算在没有 JavaScript 打包器的情况下在浏览器环境中使用 bignumber.js,你也可以通过 CDN 访问它:

const BigNumber = require("bignumber.js");

const distanceOfTheSun = new BigNumber('1.49597870700e11'); // 
console.log(distanceOfTheSun) // BigNumber { s: 1, e: 11, c: [ 149597870700 ] }
console.log(distanceOfTheSun.valueOf()) // 149597870700

当使用内置数字类型时,JavaScript 会将任何大于 Number.MAX_VALUE 的数值表示为 Infinity。但是,对于 bignumber.js,你可以使用任何大于 Number.MAX_VALUE 的值。

在下面的示例中,我通过将 Number.MAX_VALUE 作为字符串传递并计算其平方来创建 BigNumber 的实例。如果你使用内置的 JavaScript 数字类型做同样的事情,你会得到 Infinity

const BigNumber = require("bignumber.js");

console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.MAX_VALUE ** 2) // Infinity

const maxValue = new BigNumber(Number.MAX_VALUE.toString());
const square = maxValue.exponentiatedBy("2");

console.log(square.valueOf()); // 3.23170060713109998320439596646649e+616

const squareRoot = square.squareRoot();
console.log(squareRoot.valueOf()); // 1.7976931348623157e+308
console.log(squareRoot.isEqualTo(maxValue)); // true

但是,当处理无法在 JavaScript 中表示的如此大的数字时,请使用 toStringvalueOf 方法以字符串形式访问计算结果。

toNumber 方法会将你的计算结果强制转换为 JavaScript 数字类型。你仍然会遇到上面突出显示的相同 JavaScript 大数问题。你的答案将失去精度,或者 JavaScript 会将其表示为 Infinity

虽然我们在本文中的目标是使用 bignumber.js 包来处理大数,但 bignumber.js 也可以处理相应的小数。它有几个我没有在这里突出展示的内置方法。你可以通过查看文档以了解其他内置函数。

使用 JS Big Decimal 管理大数

JS Big Decimal 是另一个可用于处理大数的 JavaScript 库。与上面的同类库不同,JS Big Decimal 的包体积小,并且功能有限。你可以使用它来管理大、小十进制数。

根据你的包管理器,使用以下命令之一从 npm 包仓库安装 JS Big Decimal:

# npm 
npm i js-big-decimal

# yarn
yarn add js-big-decimal

#pnpm
pnpm add js-big-decimal

与其他两个包一样,导入 BigDecimal 构造函数并创建一个实例,如下例所示。 BigDecimal 构造函数将数字或字符串作为参数并返回 BigDecimal 对象。

然后,你可以使用 getValue 方法以字符串形式访问数字的值。或者,如果要格式化输出,请使用 getPrettyValue

const BigDecimal = require("js-big-decimal");

const value = new BigDecimal('23');
console.log(value.add(new BigDecimal(2)).getValue())

JS Big Decimal 具有执行基本数学运算的函数,例如加法减法乘法除法。下面的代码演示了如何使用它们来处理大数据:

const BigDecimal = require("js-big-decimal");

const maxSafeInt = new BigDecimal(Number.MAX_SAFE_INTEGER.toString());
const divisor = new BigDecimal("2");

console.log(maxSafeInt.getPrettyValue()); // 9,007,199,254,740,991

const sum = maxSafeInt.add(maxSafeInt);
const quotient = maxSafeInt.divide(divisor);
const diff = maxSafeInt.subtract(quotient);
const product = quotient.multiply(divisor);

console.log(sum.getValue()); // 18014398509481982
console.log(quotient.getPrettyValue()); // 4,503,599,627,370,495.50000000
console.log(diff.getPrettyValue()); // 4,503,599,627,370,495.50000000
console.log(product.getPrettyValue()); // 9,007,199,254,740,991

比较在 JavaScript 中管理大数的库

并非所有库都是完美的。每个第三方库都有用例、优点和缺点。让我们通过对比它们的优缺点并探索 GitHub 星级和问题、包大小和 npm 下载等指标来比较上述第三方包。

GitHub stars 等指标与社交媒体点赞类似。你可以将其用作第三方库受欢迎程度的指标。然而,它并没有告诉你太多关于质量的信息。

同样,npm 下载统计数据也远非精确。根据 npm,下载计数是 tarball 文件的 HTTP 200 响应服务的数量。因此,下载计数包括构建服务器、镜像和机器人的自动下载。虽然 npm 下载计数不是库活跃用户的准确度量,但你可以使用它来对包进行比较。

Math.js bignumber.js JS Big Decimal
压缩后的包体积 187.93KB 8.09KB 3.88KB
依赖数量 9 0 0
GitHub stars 13.1k 6k 96
积极维护 Yes Yes Yes
文档 Good Good Good
许可证 Apache-2.0 MIT MIT
npm 周下载量 502,656 7,114,325 25,204

上述所有第三方库都是免费的开源库,具有许可。在这三者中,Math.js 是一个功能丰富的通用数学库,而另外两个是为管理大数字而创建的。

因此,Math.js 具有最大的 Gzipped 包大小。但是,如果你使用像 webpack 这样的打包器,它是可通过 tree-shake 优化的。 Math.js 和 bignumber.js 都带有几个用于管理大数和对它们执行数学运算的特性。

另一方面,JS Big Decimal 具有最小的包大小。但是,它的功能也最少。它只能执行基本的数学运算。

小结

JavaScript 在内部使用 64 位双精度二进制浮点格式来表示数字。它分配一位表示符号,11 位表示指数,53 位表示尾数。

JavaScript 分配固定位来表示双精度浮点数的不同部分。因此,它近似于安全整数范围之外的整数。同样,它使用 Infinity 表示大于 Number.MAX_VALUE 的数值,使用 -Infinity 表示它们对应的负值。

尽管内置的 BigInt 对于处理大于最大安全整数或小于最小安全整数的整数很有用,但它是有一定局限的,因为你只能执行基本的数学运算,例如加法减法乘法求幂。你不能将它与内置 Math 对象的方法一起使用;这样做会引发错误。

要在 JavaScript 中处理大数字而不会遇到上述限制,你需要第三方库,例如 Math.js、bignumber.js 和 JS Big Decimal。尽管大多数第三方库都有局限性,正如上面强调的那样,但它们具有轻而易举处理大数的功能。

猜你喜欢

转载自blog.csdn.net/p1967914901/article/details/131387361