原文: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
是一種單獨的資料型別,所以相同值的 BigInt
和 Number
並不“嚴格相等”,即 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 程式碼。
一個需要特別注意的點是,BigInt
和 Number
之間並不能進行混合運算。這其實是一件好事,因為任何隱式轉換都可能丟失資訊。例如下面這個例子:
BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? ?
複製程式碼
結果應該是什麼?我們還沒有一個很好的答案。因為 BigInt
沒有小數部分,而 Number
則不能表示超出安全整數範圍外的值;所以,對他們進行混合操作會直接報 TypeError
錯誤。
上面這個規則例外的就是前面我們提到的如 ===
,<
, >=
等,因為他們的運算結果是布林型別,不存在精度丟失的風險。
1 + 1n;
// → TypeError
123 < 124n;
// → true
複製程式碼
注意: 因為
BigInt
和Number
不支援混合運算這點,請避免用 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
也可以精確地表示這兩種型別,而且另外還提供了兩種型別化的陣列:BigInt64Array
和 BigIntUint64Array
可以用來高效地表示和輕鬆地操作這兩種型別的列表形式的資料:
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 對本文的校審。