原文:developers.google.com/web/updates…(需越墙)
作者:Mathias Bynens
译者:西楼听雨
此作是谷歌开发者网站发布的关于 BigInt 这种新的数据类型的介绍文章。(转载请注明出处)
BigInt:JavaScript 中的任意精度整型
BigInt
是 JavaScript 的一种新的数值原始类型,它可以用来表示任意精度的整数值。有了 BigInt
后,我们可以安全放心地保存和操作整数值了,即便是那些超出了 Number
的“安全整数”范围的值。本文会介绍一些关于它的使用场景,除此之外还会通过对比 Number
来介绍一些在 Chrome 67 中新引入的功能。
使用场景
如果 JavaScript 拥有了任意精度的整数,那么它将为我们解锁许多应用场景。
BigInt
可以为我们正确地执行整数运算,不会有数值溢出的问题。光这一点就可以为我们带来无数的新的可能。尤其在金融技术领域,大数值的数学运算是经常会用到的,例如:
在 JavaScript 中,Number
是无法安全地用来表示“超大的整数形式的 ID“和“高精度时间戳“的。所以经常会导致实实在在的现实中的问题,最后开发人员都被迫改为使用 string 来表示。在有了 BigInt
后,这些数据就可以以数字值来表示了。
BigInt
还可以用来作为 BigDecimal
的一种实现。这对于带有小数的金额的求和及运算会非常有用(这里指的就是那个熟知的 0.10 + 0.20 !== 0.30
问题)。
在这之前,涉及到这些应用场景的 JavaScript 应用程序都得寻求可以模拟 BigInt
功能的那些用户自己实现的第三方库的帮助。而在 BigInt
得到广泛支持之后,这样的应用程序就可以丢弃这些运行时的依赖了(译:即第三方库)。这可以帮助我们减少加载时间、解析时间,以及编译时间,而且也可以为我们带来明显的运行时性能的提升。

从上图我们可以看出,Chrome 本地的 BigInt 性能优于流行的第三方库。
如果要对 BigInt
做“垫片(Polyfilling)”处理,需要有一个实现了相同功能的运行时库,以及一个可以将新式语法转换成对这个库的 API 的调用的转换步骤。目前 Babel 已经通过一个插件实现了对 BigInt
字面量解析(literal)的支持,但还不支持语法的转换。因此现在我们还不建议将 BigInt
投入到那些对跨浏览器兼容性有广泛支持要求的生产环境中。虽然现在还是开始阶段,不过它的功能已经开始在各家浏览器中布局了。相信对 BigInt
广泛支持的时刻应该不久就会到来。
Number 的现状
Number
在 JavaScript 中被用于表示双精度浮点类型的值。
这就意味着它存在精度上的局限。Number.MAX_SAFE_INTEGER
常量 的值意义在于表示可以被安全的加1的最大的整数值,它的值是 2**53-1
。(译:这里的两个*不是错误的写法,而是一种用来表示次方的语法)
const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991
注意:考虑到可读性,我们将大额的数值以下划线做为分隔符按千为单位进行了分割显示。
如果对其进行加 1 运算,得到的结果将是:
max + 1;
// → 9_007_199_254_740_992 ✅
但如果我们对其再次加 1,理论上的应该得到结果就不再是 Number 可以准确表示的了:
max + 2;
// → 9_007_199_254_740_992 ❌
你应该注意的了上面两段代码得出的结果都是一样的。所以每次我们在 JavaScript 中得到这样一个值的时候,我们没有办法知道他是否是正确的。所有超出了安全的整数范围(safe integer range)的计算都可能是不准确的。所以我们只能信任处于安全范围内的整型数值。
新明星 : BigInt
BigInt
是 JavaScript 中的一种新的数值原始数据类型,它可以用来表示任意精度的整形值。
要创建一个 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
),但我们得到的结果的尾数却是 0,显然这是不正确的!我们试下改为用 BigInt
:
1234567890123456789n * 123n;
// → 151851850485185185047n ✅
这次我们得到的结果才是正确的。
因为 BigInt
不存在 Number
的“安全整数”范围的限制,因此我们可以毫无顾忌地对其进行算数运算,不用担心精度丢失的问题。
一种新的原始类型
BigInt
是 JavaScript 语言里的一个新的原始数据类型,所以它也有自己的类型(type),我们可以通过 typeof
操作符来探测一下:
typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'
因为 BigInt
是一种单独的数据类型,所以相同值的 BigInt
和 Number
并不“严格相等”,即 42n !== 42
。如要对他们进行比较,可以先将一方先转换为另一方的数据类型,或者使用“抽象相等”操作符(==
)来进行判断:
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
值,例如:-42
;而一元操作符 +
则不可用,因为在 asm.js 中 +x
始终得到的是一个 Number
或者一个异常,所以他可能会破坏掉 asm.js 代码。
一个需要特别注意的点是,BigInt
和 Number
之间并不能进行混合运算。这其实是一件好事,因为任何隐式转换都可能丢失信息。例如下面这个例子:
BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔
结果应该是什么?我们还没有一个很好的答案。因为 BigInt
没有小数部分,而 Number
则不能表示超出安全整数范围外的值;所以,对他们进行混合操作会直接报 TypeError
错误。
上面这个规则例外的就是前面我们提到的如 ===
,<
, >=
等,因为他们的运算结果是布尔类型,不存在精度丢失的风险。
1 + 1n;
// → TypeError
123 < 124n;
// → true
注意: 因为
BigInt
和Number
不支持混合运算这点,请避免用 BigInt 来重写,或者意外地“升级”现有的代码。请在确认好两者所应用的范围后,才开始入手。对于那些后续新增的需要进行大额数值操作的 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
包装成“带符号整型”或者“无符号整型”的数值,我们有两个函数可以使用。一个是 BigInt.asIntN(width, value)
,它的功能是将一个 BigInt
值包装成一个 width
值大小长度的二进制“带符号整型”数值;另一个是 BigInt.asUintN(width, value)
,它的功能则是将一个 BigInt
包装成一个 width 值大小长度的二进制“无符号整型”数值。假设你现在想进行64位的算术运算,你就可以通过这两个 API 来确保运算是在期望的(数值)范围内进行的:
// “带符号的64位整型”的最大值
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
→ 9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ 因为出现了数值溢出,这里变成了负数
注意上面代码中只要我们传的参数的值超过了64位整型的最大值时,就会发生数值溢出。
还有,在其他语言中“带符号64位整型”及“无符号64位整型”属于常用的类型,(从上面的例子中可以看出)现在 BigInt
也可以精确地表示这两种类型,而且另外还提供了两种类型化的数组:BigInt64Array
和 BigIntUint64Array
可以用来高效地表示和轻松地操作这两种类型的列表形式的数据:
const view = new BigInt64Array(4);
// → [0n, 0n, 0n, 0n]
view.length;
// → 4
view[0];
// → 0n
view[0] = 42n;
view[0];
// → 42n
BigInt64Array
也会确保它里面的每一个元素的值都是“带符号的64位整型”值:
// “带符号的64位整型”的最大值
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位”的限制。
谢谢观赏,祝您和 BigInt
玩的愉快!
鸣谢:非常感谢 BigInt 规范的主导者 Daniel Ehrenberg 对本文的校审。