BigInt:JavaScript 中的任意精度整數

發布於 · 標籤為 ECMAScript ES2020

BigInt 是 JavaScript 中的新數值基本型別,可以用於表示任意精度的整數。有了 BigInt,即使超過 Number 的安全整數限制,您也能安全地儲存和運算大型整數。本文將介紹一些使用案例,並透過將 JavaScript 中的 BigIntNumber 進行比較,說明 Chrome 67 中的新功能。

使用案例 #

任意精度整數為 JavaScript 解鎖許多新的使用案例。

BigInt 使得正確執行整數運算而不會溢位成為可能。這本身就開啟了無數新的可能性。例如,金融科技中經常使用大型數字的數學運算。

大型整數 ID高精度時間戳記 無法安全地表示為 JavaScript 中的 Number。這 經常 導致 實際錯誤,並導致 JavaScript 開發人員將它們表示為字串。有了 BigInt,現在可以將這些資料表示為數值。

BigInt 可以構成最終 BigDecimal 實作的基礎。這對於表示具有小數精度的金額,以及準確地對其進行運算(又稱為 0.10 + 0.20 !== 0.30 問題)很有用。

以前,具有這些使用案例的 JavaScript 應用程式必須使用模擬類似 BigInt 功能的使用者空間函式庫。當 BigInt 廣泛可用時,此類應用程式可以放棄這些執行時間相依關係,轉而使用原生 BigInt。這有助於減少載入時間、剖析時間和編譯時間,最重要的是提供了顯著的執行時間效能改善。

Chrome 中的原生 BigInt 實作效能優於熱門的使用者空間函式庫。

現狀:Number #

JavaScript 中的數字以 雙精度浮點數 表示。這表示它們的精度有限。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_INTEGERNumber.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 ❌

查看最低有效位數,93,我們知道乘法的結果應以 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。若要將 BigIntNumber 進行比較,請在比較之前將其中一個轉換為另一種類型,或使用抽象相等性(==

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 值,例如 -42n。一元 + 受支援,因為這會中斷預期 +x 永遠會產生 Number 或例外的 asm.js 程式碼。

需要注意的一點是,不允許混合 BigIntNumber 之間的運算。這是一件好事,因為任何隱式強制轉換都可能會遺失資訊。考慮以下範例

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

結果應該是什麼?這裡沒有好的答案。BigInt 無法表示分數,而 Number 無法表示超過安全整數限制的 BigInt。因此,混合 BigIntNumber 之間的運算會導致 TypeError 例外。

這個規則唯一的例外是比較運算子,例如 ===(如前所述)、<>=,因為它們會傳回布林值,因此不會有精確度損失的風險。

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

由於 BigIntNumber 通常不會混用,請避免過度載入或神奇地「升級」現有程式碼,以使用 BigInt 取代 Number。決定要在這兩個網域中的哪一個進行操作,然後堅持下去。對於可能處理大型整數的 API,BigInt 是最佳選擇。對於已知在安全整數範圍內的整數值,Number 仍然有意義。

另一點要注意的是 >>> 算子,它執行無符號右移,對於 BigInt 沒有意義,因為它們始終是有符號的。基於這個原因,>>> 不適用於 BigInt

API #

有幾個新的 BigInt 專用 API 可用。

全域 BigInt 建構函式類似於 Number 建構函式:它將其引數轉換為 BigInt(如前所述)。如果轉換失敗,它會擲回 SyntaxErrorRangeError 例外。

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)BigInt 值包裝為 width 位元二進制無符號整數。例如,如果您正在執行 64 位元算術,則可以使用這些 API 保持在適當的範圍內

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ negative because of overflow

請注意,一旦我們傳遞超過 64 位元整數範圍的 BigInt 值(即絕對數字值 63 位元 + 符號 1 位元),就會發生溢位。

BigInt 可以準確表示 64 位元有符號和無符號整數,這些整數通常用於其他程式語言。兩種新的類型化陣列類型,BigInt64ArrayBigUint64Array,讓有效表示和操作此類值的清單變得更容易

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

BigInt64Array 類型確保其值保持在有符號 64 位元限制內。

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
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
// ^ negative because of overflow

BigUint64Array 類型使用無符號 64 位元限制執行相同的操作。

Polyfilling 和轉譯 BigInt #

撰寫本文時,BigInt 僅在 Chrome 中受支援。其他瀏覽器正積極實作中。但如果你想在現在使用 BigInt 功能,而不犧牲瀏覽器相容性,該怎麼辦?很高興你問了!答案是……至少可以說是很有趣。

與大多數其他現代 JavaScript 功能不同,BigInt 無法合理地轉譯成 ES5。

BigInt 提議 變更了運算子的行為(例如 +>= 等),以在 BigInt 上運作。這些變更無法直接 polyfill,而且也讓使用 Babel 或類似工具將 BigInt 程式碼轉譯成後備程式碼變得不可行(在多數情況下)。原因是,這種轉譯必須將程式中的每個運算子替換為呼叫某個函式的動作,而該函式會對其輸入執行類型檢查,這將造成無法接受的執行時間效能損失。此外,這會大幅增加任何轉譯套件的大小,對下載、剖析和編譯時間造成負面影響。

更可行且更具未來性的解決方案是,使用 JSBI 函式庫撰寫程式碼。JSBI 是 V8 和 Chrome 中 BigInt 實作的 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 部落格文章,其中包含實作詳細資料

BigInt 支援 #