[译]BigInt:JavaScript 中的任意精度整型

3,132 阅读10分钟

原文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 是一种单独的数据类型,所以相同值的 BigIntNumber 并不“严格相等”,即 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 代码。

一个需要特别注意的点是,BigIntNumber 之间并不能进行混合运算。这其实是一件好事,因为任何隐式转换都可能丢失信息。例如下面这个例子:

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

结果应该是什么?我们还没有一个很好的答案。因为 BigInt 没有小数部分,而 Number 则不能表示超出安全整数范围外的值;所以,对他们进行混合操作会直接报 TypeError 错误。

上面这个规则例外的就是前面我们提到的如 ===<, >= 等,因为他们的运算结果是布尔类型,不存在精度丢失的风险。

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

注意: 因为 BigIntNumber 不支持混合运算这点,请避免用 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 也可以精确地表示这两种类型,而且另外还提供了两种类型化的数组:BigInt64ArrayBigIntUint64Array 可以用来高效地表示和轻松地操作这两种类型的列表形式的数据:

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 对本文的校审。