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

西樓聽雨發表於2019-03-03

原文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 得到廣泛支援之後,這樣的應用程式就可以丟棄這些執行時的依賴了(譯:即第三方庫)。這可以幫助我們減少載入時間、解析時間,以及編譯時間,而且也可以為我們帶來明顯的執行時效能的提升。

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

從上圖我們可以看出,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 對本文的校審。

相關文章