BigInt: JavaScript 中任意精度的整数
BigInt
是 JavaScript 中的一种新型数字原始值,可以表示任意精度的整数。通过 BigInt
,您可以安全地存储并操作即使超出 Number
安全整数限制的大整数。本文通过对比 JavaScript 中的 BigInt
和 Number
,逐步介绍它的一些使用场景,并说明 Chrome 67 中的新功能。
使用场景
任意精度的整数为 JavaScript 解锁了许多新场景。
BigInt
能够正确地执行整数运算而不会溢出。这本身就开启了无数新可能。例如,在金融科技中,大数的数学运算是常见的应用场景。
大整数 ID 和 高精度时间戳 无法在 JavaScript 中安全地表示为 Number
。这经常导致现实中的 bug,迫使 JavaScript 开发者用字符串来表示这些数据。有了 BigInt
,现在可以将这些数据表示为数值。
BigInt
可以为未来的 BigDecimal
实现奠定基础。这将有助于以小数精度表示金额并准确操作这些金额(即所谓的 0.10 + 0.20 !== 0.30
问题)。
以前,拥有这些使用场景的 JavaScript 应用必须依赖用户空间的库来模拟类似 BigInt
的功能。当 BigInt
广泛可用后,这些应用可以用原生的 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_INTEGER
到 Number.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 ❌
观察最低有效位的数字 9
和 3
,我们知道乘法的结果应以 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
。要比较BigInt
与Number
,可以将其中一个转换为另一个的类型后再进行比较,或者使用抽象相等==
:
42n === BigInt(42);
// → true
42n == 42;
// → true
在转换为布尔值时(例如使用if
、&&
、||
或Boolean(int)
),BigInt
与Number
遵循相同的逻辑。
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
要么抛出异常。
一个需要注意的是,不允许在BigInt
和Number
之间混合操作。这是一个好事,因为任何隐式强制转换都可能丢失信息。以下示例展示了这种情况:
BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔
结果应该是什么?没有好的答案。BigInt
无法表示分数,而Number
无法表示超出安全整数限制的BigInt
。因此,在BigInt
和Number
之间混合操作会导致TypeError
异常。
唯一的例外是比较运算符,例如===
(如前所述),<
和>=
——因为它们返回布尔值,没有精度丢失的风险。
1 + 1n;
// → TypeError
123 < 124n;
// → true
由于BigInt
和Number
通常不能混合使用,请避免重载或魔法般“升级”现有代码以使用BigInt
代替Number
。选择其中一个领域进行操作,然后保持一致。对于操作潜在大整数的新API,BigInt
是最佳选择。而对于已知在安全整数范围内的整数值,Number
仍然是合理的选择。
另一个需要注意的是,>>>
运算符,执行无符号右移,对于始终有符号的BigInt
来说没有意义。因此,>>>
不适用于BigInt
。
API
有一些新的BigInt
特定API可用。
全局BigInt
构造函数类似于Number
构造函数:它将其参数转换为BigInt
(如前所述)。如果转换失败,则抛出SyntaxError
或RangeError
异常。
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位有符号和无符号整数。两种新的类型数组形式,BigInt64Array
和 BigUint64Array
,使得高效表示和操作此类值的列表变得更容易:
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 博客文章。