JavaScript 中的表示任意精度的 BigInt

5,898 阅读6分钟

作为前端开发,不知道大家是否被大整数困扰过?JavaScript 对大整型一直没有支持,想要操作大整型数字必须借助第三方库,除了麻烦还可能有打包过大和运行时效率的问题。对比 Java 中,早就有了能表示任意精度的BigInteger。而对于 JavaScript,ECMAScript 中的提案 BigInt 就是一个可以表示任意精度的新的数字原始类型

本文主要围绕 BigInt 讲讲其现状、特性、进展和目前的使用方法。

Number 类型的局限性

JavaScript 中的 Number 是双精度浮点型,这意味着精度有限。Number.MAX_SAFE_INTEGER 就是安全范围内的最大值,为 2**53-1。最小安全值为 Number.MIN_SAFE_INTEGER 值为 -((2**53)-1)。超出安全值的计算都会丧失精度。如下,可以看到 max + 1max + 2 的值相同,这显然是不对的。

const max = Number.MAX_SAFE_INTEGER; // 9007199254740991
max + 1 // 9007199254740992
max + 2 // 9007199254740992

至于为什么最大安全值是 2**53-1,与 IEEE 754 的 float 浮点数存储有关,可参考抓住数据的小尾巴 - JS浮点数陷阱及解法

实际应用中,例如在大整数 ID、高精度时间戳中会导致不安全的问题。Twitter IDs (snowflake)文中说到 Twitter 的 id 生成服务,当 id 持续增长时,就会超出 JS 的安全范围,因此要求同时冗余地返回字符串型的 id。另一个例子,高精度时间戳在运算的时候也会丧失精度,例如使用 performance 对象与 BigInt 结合,可以获取精确到皮秒的时间戳(当然这个时间戳是不是真的精准是另一个问题),代码如下:

// 1 毫秒(ms) = 1,000 微秒(μs) = 1,000,000 纳秒(ns) = 1,000,000,000 皮秒(ps)
const scale = 1000000000
const scaleBig = 1000000000n
const big = BigInt((performance.now() * scale).toFixed(0)) + BigInt(performance.timing.navigationStart) * scaleBig
const normal = (performance.now() + performance.timing.navigationStart) * scale
console.log(big) // 1550488515092440117252n 精确到皮秒
console.log(normal) // 1.550488515092455e+21 精确到微秒

在没有 BigInt 的时候,如果想要使用大整型,则不得不借助类似 BigInt 功能的第三方库。这有可能会影响 JavaScript 程序的效率,比如加载时间、解析时间、编译时间,以及运行时的效率。下图为 BigInt 与其他类似第三方库的性能对比。

BigInt 与其他类似第三方库的性能对比

BigInt 的特性

BigInt 是一个新的原始类型,可以实现任意精度计算。创建 BigInt 类型的值也非常简单,只需要在数字后面加上 n 即可。例如,789 变为 789n。也可以使用全局方法 BigInt(value) 转化,入参 value 为数字或数字字符串。例如:

BigInt(1234567890) === 1234567890n // true

另一个例子就是上述的时间戳转换。

新的原始类型

既然 BigInt 是一个新的原始类型,那么它就可以使用 typeof 检测出自己的类型

typeof 111 // "number"
typeof 111n // "bigint"

同时 BigIntNumber 类型的值也是不严格相等的。

111 === 111n // false
111 == 111n // true

在数字布尔间的逻辑中,BigIntNumber 表现一致。

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

如果算上 BigInt,JavaScript 中原始类型就从 6 个变为了 7 个。

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (new in ECMAScript 2015)
  • BigInt (new in future ECMAScript)

运算

BigInt 支持绝大部分常用运算符,+, -, *, /, %, 和 **

位运算符 |, &, <<, >>, ^ 表现也与 Number 类型中一致。

一元运算符 - 表示负数,但是 + 不能用于表示正数。因为在 webAssembly(asm.js) 中,+x 始终表示一个 Number 或异常情况。

另外就是不能混合使用 BigIntNumber 计算,例如下面的结果会抛出异常:

1 + 1n
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

由于不能混合使用 BigIntNumber,你也不能图省事将代码中所有的 Number 都用 BigInt 代替。需要视情况而定,如果数字有可能变得很大,那么再决定使用 BigInt

API

  • BigInt() 构造函数,类似 Number(),可以将入参转化为 BigInt 类型。

    BigInt(1) // 1n
    BigInt(1.5) // RangeError
    BigInt('1.5') // SyntaxError
    
  • BigInt64Array 和 BigUint64Array

    同时 BigInt 也可以精确表示64位有符号和无符号整型,所有有两个新的 TypedArray 即 BigInt64Array 和 BigUint64Array。

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

ECMAScript TC39 进展

目前 ES2019 的新特性都已经确定,见 Twitter -New JavaScript features in ES2019,没有 BigInt,如下图:

➡️ Array#{flat,flatMap}
➡️ Object.fromEntries
➡️ String#{trimStart,trimEnd}
➡️ Symbol#description
➡️ try { } catch {} // optional binding
➡️ JSONECMAScript
➡️ well-formed JSON.stringify
➡️ stable Array#sort
➡️ revised Function#toString

同时可以在 github 上 tc39 已完成的草案中看到。

BigInt 目前处于 Stage 3 阶段,问题不大的话,ES2020 中应该被收录。

支持情况 & PolyFill

目前(201902)浏览器支持情况并不理想,只有 Chrome 支持较好,其他浏览器支持不好。由于和其他 JavaScript 新特性不同,BigInt 不能很好的被编译为 ES5。因为 BigInt 中修改了运算符的工作行为,这些行为是不能直接被 polyfill 转换的。

但是可以使用一个库 the JSBI library,来实现 BigInt。JSBI 是直接使用了 V8 和 Chrome 中 BigInt 的设计和实现方式,功能与浏览器中一致,语法稍有不同:

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 插件 babel-plugin-transform-jsbi-to-bigint移除 JSBI 转为原生的 BigInt 语法。例如上述代码会被转为:

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

TypeScript 支持

TypeScript 3.2 已经加入了 BigInt 的类型校验。将 tsconfig 配置为 target: esnext 即可。用法示例如下:

let foo: bigint = BigInt(100); // the BigInt function
let bar: bigint = 100n;        // a BigInt literal

// *Slaps roof of fibonacci function*
// This bad boy returns ints that can get *so* big!
function fibonacci(n: bigint) {
  let result = 1n;
  for (let last = 0n, i = 0n; i < n; i++) {
    const current = result;
    result += last;
    last = current;
  }
  return result;
}

fibonacci(10000n)

小结

如果你确定你的页面只跑在最新的 Chrome 中,那么现在就可以大胆的使用 BigInt 了,更优雅高效的处理大数据。若在其他浏览器中需要支持,可以使用 JSBI 这个库,日后甩掉它的姿势也十分优雅。

看着 JavaScript 越来越健壮,甚是欣喜。随着端计算能力的强大,AI 的发展,说不定很快就能用到这个 BigInt 特性了。

参考