跳到主要内容

BigInt: JavaScript 中任意精度的整数

· 阅读需 9 分钟
Mathias Bynens ([@mathias](https://twitter.com/mathias))

BigInt 是 JavaScript 中的一种新型数字原始值,可以表示任意精度的整数。通过 BigInt,您可以安全地存储并操作即使超出 Number 安全整数限制的大整数。本文通过对比 JavaScript 中的 BigIntNumber,逐步介绍它的一些使用场景,并说明 Chrome 67 中的新功能。

使用场景

任意精度的整数为 JavaScript 解锁了许多新场景。

BigInt 能够正确地执行整数运算而不会溢出。这本身就开启了无数新可能。例如,在金融科技中,大数的数学运算是常见的应用场景。

大整数 ID高精度时间戳 无法在 JavaScript 中安全地表示为 Number。这经常导致现实中的 bug,迫使 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)),BigIntNumber遵循相同的逻辑。

if (0n) {
console.log('if');
} else {
console.log('else');
}
// → 输出 'else',因为`0n`为假值。

运算符

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)将其封装为width位二进制无符号整数。例如,如果进行64位算术运算,可以使用这些API保持在适当范围内:

// 能够表示为64位有符号整数的最大BigInt值。
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
// → 9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ 因为溢出变为负数

注意,当我们传递一个超出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位限制内。

// 可以表示为有符号64位整数的最高可能的 BigInt 值。
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
// ^ 因为溢出而变为负数

BigUint64Array 类型则使用无符号64位限制来做相同操作。

Polyfill 和转换 BigInt

在撰写本文时,BigInt 仅在 Chrome 中支持。其他浏览器正在积极实现该功能。但是如果你希望 今天 使用 BigInt 功能而不牺牲浏览器兼容性,该怎么办?我很高兴你问了!答案可以说是…相当有趣。

与其他现代 JavaScript 特性不同,BigInt 不可能合理地转换为 ES5。

BigInt 提案 改变了运算符的行为(比如 +, >= 等),以支持 BigInt。这些改变无法直接 Polyfill,也使得在大多数情况下使用 Babel 或类似工具将 BigInt 代码转换为回退代码变得不可行。原因是这样的转换必须替换程序中的 每一个运算符 为调用某个函数以对输入执行类型检查,这会导致无法接受的运行时性能损失。此外,它会极大地增加任何转换后的代码包的文件大小,负面影响下载、解析和编译时间。

一个更可行且具有前瞻性的解决方案是暂时使用 JSBI 库 编写代码。JSBI 是 BigInt 在 V8 和 Chrome 中实现的 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 支持