給急躁的程式設計師的 JavaScript(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

18 大整數 – 任意精度的整數 [ES2020](進階)



在本章中,我們將探討 JavaScript 中的 bigints,它的儲存空間會根據需要而增長和縮減。

18.1 為什麼需要 bigints?

在 ECMAScript 2020 之前,JavaScript 處理整數的方式如下

有時,我們需要超過有號 53 位元,例如

18.2 Bigints

Bigint 是整數的新基本資料類型。Bigints 在位元中沒有固定的儲存大小;它們的大小會根據它們所代表的整數而調整

Bigint 文字是一個或多個數字的序列,後綴為 n,例如

123n

-* 等運算子會重載,並與 bigints 一起使用

> 123n * 456n
56088n

Bigints 是基本值。typeof 會為它們傳回新的結果

> typeof 123n
'bigint'

18.2.1 整數超過 53 位元

JavaScript 數字在內部表示為乘以指數的分數(有關詳細資訊,請參閱 §16.8 “背景:浮點精度”)。因此,如果我們超出最高 安全整數 253−1,仍然有 一些 可以表示的整數,但它們之間有間隙

> 2**53 - 2 // safe
9007199254740990
> 2**53 - 1 // safe
9007199254740991

> 2**53 // unsafe, same as next integer
9007199254740992
> 2**53 + 1
9007199254740992
> 2**53 + 2
9007199254740994
> 2**53 + 3
9007199254740996
> 2**53 + 4
9007199254740996
> 2**53 + 5
9007199254740996

Bigints 使我們能夠超過 53 位元

> 2n**53n
9007199254740992n
> 2n**53n + 1n
9007199254740993n
> 2n**53n + 2n
9007199254740994n

18.2.2 範例:使用 bigints

以下是使用 bigints 的樣子(程式碼基於提案中的範例)

/**
 * Takes a bigint as an argument and returns a bigint
 */
function nthPrime(nth) {
  if (typeof nth !== 'bigint') {
    throw new TypeError();
  }
  function isPrime(p) {
    for (let i = 2n; i < p; i++) {
      if (p % i === 0n) return false;
    }
    return true;
  }
  for (let i = 2n; ; i++) {
    if (isPrime(i)) {
      if (--nth === 0n) return i;
    }
  }
}

assert.deepEqual(
  [1n, 2n, 3n, 4n, 5n].map(nth => nthPrime(nth)),
  [2n, 3n, 5n, 7n, 11n]
);

18.3 Bigint 文字

與數字文字一樣,bigint 文字支援多個進位

負 BigInt 是透過加上一元減號運算子來產生:-0123n

18.3.1 在 BigInt 文字中使用底線 (_) 作為分隔符號 [ES2021]

就像在數字文字中一樣,我們可以在 BigInt 文字中使用底線 (_) 作為分隔符號

const massOfEarthInKg = 6_000_000_000_000_000_000_000_000n;

BigInt 經常被用於金融技術部門中表示金錢。分隔符號在這裡也可以派上用場

const priceInCents = 123_000_00n; // 123 thousand dollars

與數字文字一樣,有兩個限制

18.4 重複使用數字運算子來處理 BigInt(重載)

對於大多數運算子,我們不允許混合 BigInt 和數字。如果我們這樣做,就會引發例外狀況

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

這項規則的原因是,沒有通用的方式可以將數字和 BigInt 轉換為共用類型:數字無法表示超過 53 位元的 BigInt,BigInt 無法表示小數。因此,例外狀況會警告我們可能會導致意外結果的拼寫錯誤。

例如,以下表達式的結果應該是 9007199254740993n 還是 9007199254740992

2**53 + 1n

以下表達式的結果應該是什麼也不清楚

2n**53n * 3.3

18.4.1 算術運算子

二元 +、二元 -*** 的運作方式符合預期

> 7n * 3n
21n

混合 BigInt 和字串是沒問題的

> 6n + ' apples'
'6 apples'

/% 會四捨五入到零(就像 Math.trunc()

> 1n / 2n
0n

一元 - 的運作方式符合預期

> -(-64n)
64n

BigInt 不支援一元 +,因為許多程式碼依賴於它將運算元轉換為數字

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

18.4.2 排序運算子

排序運算子 <>>=<= 的運作方式符合預期

> 17n <= 17n
true
> 3n > -1n
true

比較 BigInt 和數字不會構成任何風險。因此,我們可以混合 BigInt 和數字

> 3n > -1
true

18.4.3 位元運算子

18.4.3.1 數字的位元運算子

位元運算子會將數字解釋為 32 位元整數。這些整數可以是無號數或有號數。如果是帶有號數,整數的負數就是它的二補數(將整數加到它的二補數中,同時忽略溢位,會產生零)

> 2**32-1 >> 0
-1

由於這些整數具有固定的位元大小,因此它們的最高位元表示它們的符號

> 2**31 >> 0 // highest bit is 1
-2147483648
> 2**31 - 1 >> 0 // highest bit is 0
2147483647
18.4.3.2 BigInt 的位元運算子

對於 BigInt,位元運算子會將負號解釋為無限的二補數,例如

也就是說,負號更像是一個外部標誌,而不是實際位元表示。

18.4.3.3 位元非 (~)

按位取反 (~) 會反轉所有位元

> ~0b10n
-3n
> ~0n
-1n
> ~-2n
1n
18.4.3.4 二進制按位元運算子 (&|^)

將二進制按位元運算子套用於大整數時,其運作方式類似於將其套用於數字

> (0b1010n |  0b0111n).toString(2)
'1111'
> (0b1010n &  0b0111n).toString(2)
'10'

> (0b1010n | -1n).toString(2)
'-1'
> (0b1010n & -1n).toString(2)
'1010'
18.4.3.5 按位元有號位移運算子 (<<>>)

大整數的有號位移運算子會保留數字的符號

> 2n << 1n
4n
> -2n << 1n
-4n

> 2n >> 1n
1n
> -2n >> 1n
-1n

請記住,-1n 是一連串向左延伸至無限大的 1。這就是為什麼將其向左位移不會改變它的原因

> -1n >> 20n
-1n
18.4.3.6 按位元無號右位移運算子 (>>>)

大整數沒有無號右位移運算子

> 2n >>> 1n
TypeError: BigInts have no unsigned right shift, use >> instead

為什麼?無號右位移背後的想法是將一個零從「左邊」移入。換句話說,假設是有有限數量的二進制數字。

然而,對於大整數而言,沒有「左邊」,其二進制數字會無限延伸。這對於負數來說尤其重要。

有號右位移運算子即使在有無限個數字時也能運作,因為最高位元會被保留。因此,它可以調整為大整數。

18.4.4 寬鬆相等 (==) 和不等 (!=)

寬鬆相等 (==) 和不等 (!=) 會強制轉換值

> 0n == false
true
> 1n == true
true

> 123n == 123
true

> 123n == '123'
true

18.4.5 嚴格相等 (===) 和不等 (!==)

嚴格相等 (===) 和不等 (!==) 僅在值具有相同類型時才將值視為相等

> 123n === 123
false
> 123n === 123n
true

18.5 包裝器建構函式 BigInt

類似於數字,大整數具有相關的包裝器建構函式 BigInt

18.5.1 BigInt 作為建構函式和函式

表 13:將值轉換為大整數。
x BigInt(x)
未定義 會擲出 TypeError
null 會擲出 TypeError
布林值 false 0ntrue 1n
數字 範例:123 123n
非整數 會擲出 RangeError
大整數 x(不變更)
字串 範例:'123' 123n
無法解析 會擲出 SyntaxError
符號 會擲出 TypeError
物件 可設定(例如透過 .valueOf()
18.5.1.1 轉換 undefinednull

如果 xundefinednull,則會擲出 TypeError

> BigInt(undefined)
TypeError: Cannot convert undefined to a BigInt
> BigInt(null)
TypeError: Cannot convert null to a BigInt
18.5.1.2 轉換字串

如果字串不表示整數,BigInt() 會擲回 SyntaxError(而 Number() 會傳回錯誤值 NaN

> BigInt('abc')
SyntaxError: Cannot convert abc to a BigInt

後綴 'n' 不被允許

> BigInt('123n')
SyntaxError: Cannot convert 123n to a BigInt

允許 bigint 文字的所有進制

> BigInt('123')
123n
> BigInt('0xFF')
255n
> BigInt('0b1101')
13n
> BigInt('0o777')
511n
18.5.1.3 非整數會產生例外
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to a BigInt because
it is not an integer
> BigInt(123)
123n
18.5.1.4 轉換物件

可以設定如何將物件轉換成 bigint,例如覆寫 .valueOf()

> BigInt({valueOf() {return 123n}})
123n

18.5.2 BigInt.prototype.* 方法

BigInt.prototype 儲存原始 bigint「繼承」的方法

18.5.3 BigInt.* 方法

18.5.4 轉換和 64 位元整數

轉換讓我們可以用特定位元數建立整數值。如果我們想限制自己只使用 64 位元整數,我們必須總是進行轉換

const uint64a = BigInt.asUintN(64, 12345n);
const uint64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, uint64a * uint64b);

18.6 將 bigint 轉換為其他原始型別

此表格顯示如果我們將 bigint 轉換為其他原始型別會發生什麼事

轉換為 明確轉換 轉換(隱式轉換)
布林值 Boolean(0n) false !0n true
Boolean(int) true !int false
數字 Number(7n) 7(範例) +int TypeError(1)
字串 String(7n) '7'(範例) ''+7n '7'(範例)

註腳

18.7 TypedArray 和 DataView 作業,用於 64 位元值

拜 bigint 所賜,TypedArray 和 DataView 可以支援 64 位元值

18.8 bigint 和 JSON

JSON 標準是固定的,不會變更。好處是舊的 JSON 解析程式碼永遠不會過時。壞處是 JSON 無法延伸來包含 bigint。

將 bigint 字串化會擲回例外

> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
> JSON.stringify([123n])
TypeError: Do not know how to serialize a BigInt

18.8.1 將 bigint 字串化

因此,我們最好的選擇是將 bigint 儲存在字串中

const bigintPrefix = '[[bigint]]';

function bigintReplacer(_key, value) {
  if (typeof value === 'bigint') {
    return bigintPrefix + value;
  }
  return value;
}

const data = { value: 9007199254740993n };
assert.equal(
  JSON.stringify(data, bigintReplacer),
  '{"value":"[[bigint]]9007199254740993"}'
);

18.8.2 解析 bigint

以下程式碼顯示如何剖析字串,例如我們在先前範例中產生的字串。

function bigintReviver(_key, value) {
  if (typeof value === 'string' && value.startsWith(bigintPrefix)) {
    return BigInt(value.slice(bigintPrefix.length));
  }
  return value;
}

const str = '{"value":"[[bigint]]9007199254740993"}';
assert.deepEqual(
  JSON.parse(str, bigintReviver),
  { value: 9007199254740993n }
);

18.9 常見問題:大整數

18.9.1 我如何決定何時使用數字,何時使用大整數?

我的建議

所有現有的網路 API 只會傳回和接受數字,而且只會逐案升級到大整數。

18.9.2 為何不只是以與大整數相同的方式增加數字的精度?

可以想見,可以將 number 分割成 integerdouble,但這會為語言增加許多新的複雜性(幾個僅限整數的運算子等)。我在 Gist 中勾勒出後果。


致謝