JavaScript 之旅 (22):BigInt

留言

目錄

  1. 過去 Number
  2. 現代的 BigInt
    1. 語法
    2. 各種運算子的數學運算
    3. 數值比較
    4. 條件判斷
    5. 有些無法成功強制轉型,會拋出 exception
    6. 不能和 Number 一起使用
    7. 可與 String 進行串接
    8. BigInt() constructor 不能傳入 Number 或 String 型別的小數
  3. 資料來源

本篇介紹 ES2020 (ES11) 提供的 BigInt

本文同步發表於 iT 邦幫忙:JavaScript 之旅 (22):BigInt

「JavaScript 之旅」系列文章發文於:

過去 Number

在過去,只要 JavaScript 提到 primitive 就會想到 StringNumberBoolean,但其實還有在 ES6 提供的 Symbol,另外在 spec 的定義中還有 UndefinedNull

除了以上這些,還有本篇要介紹的新 primitive:BigInt

下面是 spec 對 primitive value 的定義:

Number 只能安全地表示最大為 $2^{53}$ 的整數,但有時會有大整數的需求,例如:

  • 社群貼文 ID (例如:Twitter ID),在 JavaScript 只能用字串儲存
  • FinTech 會需要很大的整數,且需要有任意精度的整數來表示金額

例如:在 JavaScript 竟然 2 ** 53 會等於 2 ** 53 + 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let x = 2 ** 53;
let y = 2 ** 53 + 1;

console.log(x);
// 9007199254740992
console.log(y);
// 9007199254740992

console.log(x === y);
// true

console.log(typeof x);
// number
console.log(typeof y);
// number

很奇怪吧!所以這就是 BigInt 被提出的原因!

為何會有這個現象是跟 IEEE 754 的浮點數定義有關,詳請可參閱 Daniel Ehrenberg 在 JSConf EU 2018 的 演講簡報演講影片

現代的 BigInt

BigInt 是一種可用來表示 arbitrary precision (任意精度,高精度) 的整數,也就是可以表示大於 $2^{53}$ 的整數。

例如:剛剛 2 ** 532 ** 53 + 1 的例子若改用 BigInt 就會如你預期的不一樣 (本來就不同啊 XD):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let x = 2n ** 53n;
let y = 2n ** 53n + 1n;

console.log(x);
// 9007199254740992n
console.log(y);
// 9007199254740993n

console.log(x === y);
// false

console.log(typeof x);
// bigint
console.log(typeof y);
// bigint

語法

BigInt 的語法和 Number 直接寫整數不太一樣,BigInt literal (字面值) 是在整數的後面加上 n 後綴:

1
2
3
4
5
6
let x = 9007199254740992n;

console.log(x);
//9007199254740992n
console.log(typeof x);
// bigint

另一種方式是呼叫 constructor 來建立 BigInt

1
2
3
4
5
6
let x = BigInt(9007199254740992);

console.log(x);
//9007199254740992n
console.log(typeof x);
// bigint

各種運算子的數學運算

BigInt+-***% 的運算上跟 Number 差不多 (這邊說的差不多是指整數小於 $2^{53}$ 的情況下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let x = 9007199254740992n;

console.log(x + 1n);
// 9007199254740993n
console.log(x - 1n);
// 9007199254740991n
console.log(x * 2n);
// 18014398509481984n
console.log(x * -2n);
// -18014398509481984n
console.log(x ** 2n);
// 81129638414606681695789005144064n
console.log(x % 10n);
// 2n

但在小數運算上跟 Number 不同,因為 BigInt 不是 BigDecimals,所以會 rounded towards 0,即不保留任何小數 (類似無條件捨去):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(5 / 2);
// 2.5
console.log(5n / 2n);
// 2n

console.log(5 / 3);
// 1.6666666666666667
console.log(5n / 3n);
// 1n

console.log(5 / 4);
// 1.25
console.log(5n / 4n);
// 1n

在 spec 中的 BigInt::divide ( x, y ) 就有定義 rounded towards 0:

數值比較

BigInt 不嚴格等於 (not strictly equal) Number,例如:

1
2
3
4
5
console.log(0n === 0);
// false

console.log(0n == 0);
// true

在 spec 中的「Strict Equality Comparison」有定義:

  • 步驟 1:當兩個值的型別不同時,就會回傳 false,即不嚴格等於
  • 步驟 2:其中一個值的型別為 NumberBigInt 就會回傳 Type(x)::equal(x, y)

若將上面範例中的 0n 帶入 spec,那 0n 就是 x,所以代表步驟 2 會去執行 BigInt::equal (x, y),spec 的定義如下:只要兩個值的數學整數值相等時,就會回傳 true,否則回傳 false

但可照常用 ><>=<= 進行比較,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(3n > 2);
// true
console.log(3n < 2);
// false

console.log(2 < 3n);
// true
console.log(2 > 3n);
// false

console.log(2 < 2n);
// false
console.log(2 > 2n);
// false

console.log(2 <= 2n);
// true
console.log(2 >= 2n);
// true

條件判斷

若將 BigInt 強制轉型成 Boolean,行為會跟 Number 類似。

例如:if 陳述句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (0) {
console.log('if')
} else {
console.log('else')
}
// else


if (0n) {
console.log('if')
} else {
console.log('else')
}
// else

if 陳述句會將值進行 ToBoolean() 強制轉型,spec 定義如下:

例如:二元邏輯運算子 (binary logical operator) 的 &&||

1
2
3
4
5
6
7
8
9
console.log(0 || 10);
// 10
console.log(0 && 10);
// 0

console.log(0n || 10n);
// 10n
console.log(0n && 10n);
// 0n

&&|| 會將值進行 ToBoolean() 強制轉型,spec 定義如下:

例如:使用 Boolean() constructor:

1
2
3
4
5
6
7
8
9
console.log(Boolean(0));
// false
console.log(Boolean(10));
// true

console.log(Boolean(0n));
// false
console.log(Boolean(10n));
// true

Boolean() constructor 會將值進行 ToBoolean() 強制轉型,spec 定義如下:

例如:邏輯 NOT 運算子 (logical NOT operator) !

1
2
3
4
5
6
7
8
9
console.log(!0);
// true
console.log(!10);
// false

console.log(!0n);
// true
console.log(!10n);
// false

! 會將值進行 ToBoolean() 強制轉型,spec 定義如下:

上面說的這些都將值進行 ToBoolean() 強制轉型,下面是 ToBoolean() 的定義:只有 0n 會回傳 false,其他都回傳 true (跟 Number 很像對吧?有興趣的可以看 Number 的部份)

所以這就是 0n 會變成 false 的原因!

有些無法成功強制轉型,會拋出 exception

BigInt 不能和 unary + 運算子一起使用,需先用 Number() constructor 轉換:

1
2
3
4
console.log(+1n);
// TypeError: Cannot convert a BigInt value to a number
console.log(Number(1n));
// 1

在 spec 是有定義的,因為 unary + 運算子會進行 ToNumber() 的強制轉型:

ToNumber() 只要遇到 BigInt 就會拋出 TypeError exception:

不能和 Number 一起使用

不能和 Number 一起使用,否則會拋出 TypeError

1
2
3
4
console.log(1n + 2);
// TypeError: Cannot mix BigInt and other types, use explicit conversions
console.log(1n * 2);
// TypeError: Cannot mix BigInt and other types, use explicit conversions

若要和 Number 一起使用,請先用 Number() constructor 強制轉型 (即 explicit conversions) 成 Number

1
2
3
4
console.log(Number(1n) + 2);
// 3
console.log(Number(1n) * 2);
// 2

下面是 + 運算子在 spec 中的定義,其中的步驟 8 和 9 都會將 + 左右邊的兩個運算元進行 ToNumeric() 的強制轉型:

而下面是 ToNumeric() 的定義,可以看到步驟 2,只要值為 BigInt 就直接回傳值,所以不會跑到步驟 3:

步驟 1 的 ToPrimitive() 只會對型別為 Object 的值才會有特殊行為,所以本篇會跳過這邊的細節 XD,否則篇幅會更長,在這裡可以簡單當作將 BigInt 型別的值傳給 ToPrimitive() 會回傳一樣的值,不會有任何強制轉型的行為。

接著回到 + 運算子的步驟 (圖重貼一次):

  • 在步驟 8 和 9 時,其中一個值的型別為 Number,另一個為 BigInt (因為前面的範例是 1n + 2 )
  • 在步驟 10 只要兩個值的型別不同,就會拋出 TypeError exception

所以這就是 BigInt 不能和 Number 一起使用的原因。

可與 String 進行串接

例如:

1
2
3
4
console.log(1n + '2');
// 12
console.log('2' + 1n);
// 21

BigInt() constructor 不能傳入 NumberString 型別的小數

例如:

1
2
3
4
console.log(BigInt(2.5));
// RangeError: The number 2.5 cannot be converted to a BigInt because it is not an integer
console.log(BigInt('2.5'));
// SyntaxError: Cannot convert 2.5 to a BigInt

下面是 BigInt() constructor 在 spec 的定義:

  • 步驟 2:將值進行 ToPrimitive() 強制轉型
  • 步驟 3:若值的型別為 Number,則進行 NumberToBigInt(prim) 強制轉型
  • 步驟 4:進行 ToBigInt() 強制轉型

先來看為何不能傳入 Number 型別的小數?這跟步驟 3 有關,會進行 NumberToBigInt(prim) 強制轉型,spec 的定義如下:

  • 步驟 2:若值不是整數就會拋出 RangeError exception

那為何不能傳入 String 型別的小數?這跟步驟 4 有關,會進行 ToBigInt() 強制轉型,spec 的定義如下:

  • 步驟 1:進行 StringToBigInt() 強制轉型
  • 步驟 2:若強制轉型的結果為 NaN,會拋出 SyntaxError exception

而下面是 StringToBigInt() 的定義:

所以字串會透過 StringToBigInt() 強制轉行成 NaN,就會拋出 SyntaxError exception。

資料來源

分享:

討論區