BigInt:JavaScript 中的任意精度整数

发布日期 · 标签:ECMAScript ES2020

BigInt 是 JavaScript 中的一种新的数值基本类型,可以表示任意精度的整数。使用 BigInt,您可以安全地存储和操作大型整数,即使超过 Number 的安全整数限制。本文将介绍一些用例,并通过比较 JavaScript 中的 BigIntNumber 来解释 Chrome 67 中的新功能。

用例 #

任意精度整数为 JavaScript 开启了许多新的用例。

BigInt 使得能够在不溢出的情况下正确执行整数运算。这本身就带来了无数新的可能性。例如,金融科技中经常使用对大数的数学运算。

大型整数 ID高精度时间戳 在 JavaScript 中无法安全地表示为 Number。这 经常会导致 现实世界中的错误,并导致 JavaScript 开发人员将它们表示为字符串。使用 BigInt,这些数据现在可以表示为数值。

BigInt 可以作为最终 BigDecimal 实现的基础。这将有助于表示具有小数精度的金额,并准确地对其进行操作(即 0.10 + 0.20 !== 0.30 问题)。

以前,具有这些用例的 JavaScript 应用程序必须求助于模拟 BigInt 功能的用户库。当 BigInt 广泛可用时,此类应用程序可以放弃这些运行时依赖项,转而使用本机 BigInt。这有助于减少加载时间、解析时间和编译时间,最重要的是,它提供了显著的运行时性能改进。

Chrome 中的本机 BigInt 实现比流行的用户库性能更好。

现状:Number #

JavaScript 中的 Number 表示为 双精度浮点数。这意味着它们的精度有限。Number.MAX_SAFE_INTEGER 常量给出可以安全递增的最大可能的整数。它的值为 2**53-1

const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991

注意:为了可读性,我将此大数中的数字每千位分组,使用下划线作为分隔符。 数值字面量分隔符提案 允许对常见的 JavaScript 数值字面量执行此操作。

递增一次得到预期结果

max + 1;
// → 9_007_199_254_740_992 ✅

但如果我们再递增一次,结果就无法准确地表示为 JavaScript Number

max + 2;
// → 9_007_199_254_740_992 ❌

请注意 max + 1 如何产生与 max + 2 相同的结果。无论何时我们在 JavaScript 中获得此特定值,都无法判断它是否准确。对安全整数范围之外的整数(即从 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER)的任何计算都可能会丢失精度。因此,我们只能依赖安全范围内的数值整数。

新宠:BigInt #

BigInt 是 JavaScript 中的一种新的数值基本类型,可以表示具有 任意精度 的整数。使用 BigInt,您可以安全地存储和操作大型整数,即使超过 Number 的安全整数限制。

要创建 BigInt,请在任何整数字面量后面添加 n 后缀。例如,123 变为 123n。全局 BigInt(number) 函数可用于将 Number 转换为 BigInt。换句话说,BigInt(123) === 123n。让我们使用这两种技术来解决我们之前遇到的问题

BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// → 9_007_199_254_740_993n ✅

以下是一个示例,我们在此示例中将两个 Number 相乘

1234567890123456789 * 123;
// → 151851850485185200000 ❌

查看最低有效位,93,我们知道乘法的结果应该以 7 结尾(因为 9 * 3 === 27)。但是,结果以一堆零结尾。这不可能!让我们尝试使用 BigInt 再次尝试

1234567890123456789n * 123n;
// → 151851850485185185047n ✅

这次我们得到了正确的结果。

Number 的安全整数限制不适用于 BigInt。因此,使用 BigInt,我们可以执行正确的整数运算,而不必担心丢失精度。

一种新的基本类型 #

BigInt 是 JavaScript 语言中的一种新的基本类型。因此,它们拥有自己的类型,可以使用 typeof 运算符检测该类型

typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'

由于 BigInt 是一种单独的类型,因此 BigInt 从未与 Number 严格相等,例如 42n !== 42。要将 BigIntNumber 进行比较,请在进行比较之前将其中一个转换为另一个的类型,或使用抽象相等(==

42n === BigInt(42);
// → true
42n == 42;
// → true

当强制转换为布尔值(例如,在使用 if&&||Boolean(int) 时),BigInt 遵循与 Number 相同的逻辑。

if (0n) {
console.log('if');
} else {
console.log('else');
}
// → logs 'else', because `0n` is falsy.

运算符 #

BigInt 支持最常见的运算符。二元 +-*** 按预期工作。/% 工作,并根据需要向零舍入。位运算 |&<<>>^ 执行位运算,假设负值的 二进制补码表示,就像它们对 Number 所做的那样。

(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n

一元 - 可用于表示负 BigInt 值,例如 -42n。一元 + 不支持,因为它会破坏 asm.js 代码,该代码期望 +x 始终产生 Number 或异常。

一个问题是,不允许在 BigIntNumber 之间混合运算。这是一件好事,因为任何隐式强制转换都可能会丢失信息。请考虑以下示例

BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔

结果应该是什么?这里没有好的答案。BigInt 无法表示分数,Number 无法表示超过安全整数限制的 BigInt。因此,在 BigIntNumber 之间混合运算会导致 TypeError 异常。

此规则的唯一例外是比较运算符,例如 ===(如前所述)、<>= - 因为它们返回布尔值,所以没有精度损失的风险。

1 + 1n;
// → TypeError
123 < 124n;
// → true

由于 BigIntNumber 通常不混合,请避免重载或神奇地“升级”现有代码以使用 BigInt 而不是 Number。决定要操作的这两个域中的哪一个,然后坚持使用它。对于新的对可能的大整数进行操作的 API,BigInt 是最佳选择。Number 仍然适用于已知在安全整数范围内的整数值。

需要注意的另一件事是,>>> 运算符 执行无符号右移,对于 BigInt 来说没有意义,因为它们始终是有符号的。因此,>>> 不适用于 BigInt

API #

提供了一些新的特定于 BigInt 的 API。

全局 BigInt 构造函数类似于 Number 构造函数:它将参数转换为 BigInt(如前所述)。如果转换失败,它将抛出 SyntaxErrorRangeError 异常。

BigInt(123);
// → 123n
BigInt(1.5);
// → RangeError
BigInt('1.5');
// → SyntaxError

这些示例中的第一个将数值字面量传递给 BigInt()。这是一个不好的做法,因为 Number 会遭受精度损失,因此我们可能在 BigInt 转换发生之前就已经丢失了精度

BigInt(123456789123456789);
// → 123456789123456784n ❌

因此,我们建议坚持使用 BigInt 字面量表示法(带 n 后缀),或者将字符串(而不是 Number!)传递给 BigInt()

123456789123456789n;
// → 123456789123456789n ✅
BigInt('123456789123456789');
// → 123456789123456789n ✅

两个库函数允许将 BigInt 值包装为有符号或无符号整数,限制为特定位数。BigInt.asIntN(width, value)BigInt 值包装为 width 位二进制有符号整数,BigInt.asUintN(width, value)BigInt 值包装为 width 位二进制无符号整数。例如,如果您正在执行 64 位运算,则可以使用这些 API 保持在适当的范围内

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ negative because of overflow

请注意,一旦我们传递超过 64 位整数范围(即 63 位用于绝对数值 + 1 位用于符号)的 BigInt 值,就会发生溢出。

BigInt 使得能够准确地表示 64 位有符号和无符号整数,这些整数在其他编程语言中很常见。两种新的类型化数组类型,BigInt64ArrayBigUint64Array,使高效地表示和操作此类值的列表变得更加容易

const view = new BigInt64Array(4);
// → [0n, 0n, 0n, 0n]
view.length;
// → 4
view[0];
// → 0n
view[0] = 42n;
view[0];
// → 42n

BigInt64Array 类型确保其值保持在 64 位有符号限制内。

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
const max = 2n ** (64n - 1n) - 1n;
view[0] = max;
view[0];
// → 9_223_372_036_854_775_807n
view[0] = max + 1n;
view[0];
// → -9_223_372_036_854_775_808n
// ^ negative because of overflow

BigUint64Array 类型使用无符号 64 位限制来执行相同的操作。

填充和转译 BigInts #

在撰写本文时,BigInt 仅在 Chrome 中受支持。其他浏览器正在积极努力实现它们。但是,如果您想今天使用 BigInt 功能而不牺牲浏览器兼容性怎么办?我很高兴您问!答案是……至少可以说很有趣。

与大多数其他现代 JavaScript 功能不同,BigInt 无法合理地转译为 ES5。

BigInt 提案 更改了运算符的行为(例如 +>= 等)以在 BigInt 上工作。这些更改不可能直接填充,并且在大多数情况下,它们也使得使用 Babel 或类似工具将 BigInt 代码转译为回退代码变得不可行。原因是这种转译必须用对输入执行类型检查的函数调用来替换程序中的每个运算符,这会导致不可接受的运行时性能损失。此外,它会大大增加任何转译包的文件大小,从而对下载、解析和编译时间产生负面影响。

一个更可行且更具未来性的解决方案是现在使用 JSBI 库 编写代码。JSBI 是 V8 和 Chrome 中 BigInt 实现的 JavaScript 端口 - 按照设计,它的行为与本机 BigInt 功能完全相同。区别在于它没有依赖语法,而是公开了一个 API

import JSBI from './jsbi.mjs';

const max = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
const two = JSBI.BigInt('2');
const result = JSBI.add(max, two);
console.log(result.toString());
// → '9007199254740993'

一旦您关心的所有浏览器都原生支持 BigInt,您就可以 使用 babel-plugin-transform-jsbi-to-bigint 将代码转译为本机 BigInt 代码 并放弃 JSBI 依赖项。例如,上面的示例转译为

const max = BigInt(Number.MAX_SAFE_INTEGER);
const two = 2n;
const result = max + two;
console.log(result);
// → '9007199254740993'

进一步阅读 #

如果您有兴趣了解 BigInt 在幕后是如何工作的(例如,它们在内存中的表示方式以及如何执行对它们的运算),请 阅读我们包含实现细节的 V8 博客文章

BigInt 支持 #