大整數 bigint

August 4, 2022

在〈與眾不同的資料型態〉談過,在 ES5 以前,JavaScript 沒有整數與浮點數的區別,數值都是 〈IEEE 754 標準 64 位元浮點數〉,型態為 number,如果只用 number 來表示整數,位元組中可使用的範圍就是 52 個位元的分數部份,最大整數只能表示到 2⁵³,也就是 9007199254740992,然而 9007199254740992 這數字不安全,因為寫下 9007199254740993,結果還是代表著 9007199254740992:

大整數的問題?

為了能表現更大的整數,有些第三方程式庫實現了大整數運算,可用來表示更大的整數,這樣不就好了嗎?若面對的只是數字計算,實現大整數運算的第三方程式庫或許能解決,但問題往往不那麼單純。例如看看前後端資料傳遞的問題,許多文件常提的案例就是 Twitter 的 64 位元 ID,或是高精度時間戳記,前後端溝通時,須以字串方式傳遞、轉換處理。

另外,在面對 WebAssembly、WebGL 時,會有 64 位元整數的整合問題,例如,WebAssembly 支援 i64,若要與 JavaScript 互通,參數或傳回值就必須合法化(Legalization),參數部份要將 i64 改為兩個 i32,分別表示 64 位元整數的高低位元組,i64 傳回值的部份也必須想辦法拆成兩部份,對這些有興趣的話,可以進一步參考〈WebAssembly integration with JavaScript BigInt〉。

另外,也有位元運算的問題,若對 JavaScript 的 number 進行位元運算,數字會被當成 32 位元整數,因此 2 ** 32 - 1 >> 0 結果不是 4294967295,而是 -1,因為最高位元是 1,也就是 2 補數表示整數 -1;簡單來說,問題並不只有大整數運算,有興趣可以進一步參考〈Bigints (advanced)〉。

bigint

ES11 以後在數字後接上 n,可以建立 bigint,它是基本型態,typeof 的結果是 'bigint',如果想將代表整數的 numberstring 轉換為 bigint,可以使用 BigInt 函式,如果指定的值不是代表整數,會引發 RangeError

> 19007199254740992n
19007199254740992n
> typeof 19007199254740992n
'bigint'
> BigInt(0)
0n
> BigInt('123456789987654321')
123456789987654321n
> BigInt(1.2)
Uncaught:
RangeError: The number 1.2 cannot be converted to a BigInt because it is not an integer
    at BigInt (<anonymous>)
> BigInt('1.2')
Uncaught SyntaxError: Cannot convert 1.2 to a BigInt
>

+-*/%** 等運算子,可以套用在 bigint,不過記得,bigint 是整數,/ 的結果只會得到整數的部份:

> 1n - 1n
0n
> 2n * 3n
6n
> 1n / 3n
0n
> 10n % 3n
1n
> 3n ** 2n
9n
>

bigint 是整號 &|^<<>> 位元運算子,然而不能使用 >>>

> 2n << 10n
2048n
> 2n >> 10n
0n
> 2n >>> 10n
Uncaught TypeError: BigInts have no unsigned right shift, use >> instead
>

因為 >>> 是無號右移(unsigned right shift),不管值本來最左邊是什麼位元,位移後最左邊位元直接補 0,這意謂著代表某整數值的位元數量是有限的,然而,bigint 的整數沒有上限的概念(當然,有實體記憶體的物理限制),也就是相當於位元組數量無限,也就不支援 >>> 了。

Python 不支援 >>> 也是同樣的道理,因為 Python 的整數是 int 實例,直接支援大整數。

不要想有個整數時,就隨意地使用 bigint,它的相關運算不是常數時間。

bigint 與 number?

因為 JavaScript 一直以來,都可以藉由 +,將值轉換為 number

> +'234'
234
> +true
1
>

基於相容性,bigint 不支援單元運算子 +

> +1n
Uncaught TypeError: Cannot convert a BigInt value to a number
>

有發現方才一連串的示範,都只使用 bigint 嗎?基本上 bigintnumber 在多個方面,被限制不能混合運算,會引發 TypeError,隨便舉幾個來看:

> 1 + 2n
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
> 2n ** 0.5
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
> 

===!== 嚴格比較時要記得,0 與 0n 是不同的;bigintnumber 可以混合運算的場合,在於不會發生 bigintnumber 混淆的情況,例如 >>=<<=,或者是 bigint 被作為布林值的場合,0n 會被作為成立,其他 bigint 的值會被作為不成立,也就是說〈弱型別的代價 – WAT!〉裡談過的 False family 多了 0n

> 0n === 0
false
> 10n > 8
true
> 10n && 2n
2n
> 0n && 2n
0n
> 1n && 0
0
> 1n && 0
0

bigintnumber 幾乎不能混合運算的結果就是,多數既有的程式庫,包含標準程式庫,像是 Math 上的許多函式,都不能與 bigint 搭配:

> Math.pow(10n, 2n)
Uncaught TypeError: Cannot convert a BigInt value to a number
    at Math.pow (<anonymous>)
>

有些人會說,有了 bigint,終於可以不用使用第三方大整數程式庫了,這種說法並不正確,因為 bigintnumber 幾乎不能混合運算,搭配 bigint 的程式庫還是必須存在的。

事實上,目前有個〈BigInt Math for JavaScript〉提案;在 bigint 規格底定前,其實有過一些讓 bigintnumber 互操作的討論,不過都被否決了,有興趣可參考〈ES11 的 bigint〉。

如果真的有 numberbigint 必須進行運算,可以明確地將代表整數的 number 透過 BigInt 函式轉換為 bigint;或者明確地將 bigintNumber 轉換為 number,然而要留意整數範圍問題。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter