JavaScript 中的表示任意精度的 BigInt

HyG發表於2019-03-13

作為前端開發,不知道大家是否被大整數困擾過?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
➡️ JSON ⊂ ECMAScript
➡️ 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 特性了。

參考

相關文章