BigInt:JavaScriptにおける任意精度整数
BigInt
はJavaScriptにおける新しい数値プリミティブで、任意精度の整数を表現できます。BigInt
を使用すると、Number
で安全な整数制限を超える大きな整数を安全に保存し操作することができます。本記事ではいくつかの使用例を紹介し、JavaScriptのNumber
と比較することでChrome 67における新機能を説明します。
使用例
任意精度の整数は、JavaScriptに多くの新しい使用例をもたらします。
BigInt
を使用すると、オーバーフローを防ぎながら整数演算を正確に行うことができます。このこと自体が無数の新しい可能性を生み出します。例えば、金融技術における大きな数値の数学的演算がよく使用されます。
大きな整数IDや高精度のタイムスタンプは、JavaScriptではNumber
として安全に表現することができません。このことがしばしば、現実のバグを引き起こし、JavaScriptの開発者がそれらを文字列として表現する原因となります。BigInt
を使用すれば、これらのデータを数値として表現することができます。
BigInt
は最終的なBigDecimal
実装の基礎となる可能性があります。これは小数精度で金額を表現し、それらの精度を落とさない演算(いわゆる0.10 + 0.20 !== 0.30問題
)を行うのに役立つでしょう。
これまで、JavaScriptのアプリケーションはこれらの使用例においてBigInt
のような機能をエミュレートするユーザーランドライブラリに頼る必要がありました。BigInt
が広く利用可能になると、これらのアプリケーションはランタイム依存性を削減し、ネイティブのBigInt
を選ぶことができます。これにより、ロード時間、解析時間、コンパイル時間が削減されるだけでなく、実行時の性能も大幅に向上します。
現状: Number
JavaScriptにおけるNumber
は倍精度浮動小数点として表現されます。このため、精度に限りがあります。Number.MAX_SAFE_INTEGER
定数は安全にインクリメントできる最大の整数を示します。その値は2**53-1
です。
const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991
注意: この大きな数値の数字を見やすくするため、グループ化してアンダースコアを区切り文字として使用しています。数値リテラルセパレータ提案は、通常のJavaScript数値リテラルでこれを可能にします。
1回インクリメントすると期待通りの結果が得られます。
max + 1;
// → 9_007_199_254_740_992 ✅
しかし、もう1回インクリメントすると、結果がもはやJavaScriptのNumber
として正確に表現することができなくなります。
max + 2;
// → 9_007_199_254_740_992 ❌
max + 1
とmax + 2
が同じ結果を生成することに注意してください。これにより、JavaScriptでこの特定の値が正確かどうかを判断する方法がなくなります。安全な整数範囲(Number.MIN_SAFE_INTEGER
からNumber.MAX_SAFE_INTEGER
まで)外での整数計算は精度を失う可能性があります。この理由で、安全な範囲内の数値整数値のみを信頼することができます。
新たな革新: BigInt
BigInt
はJavaScriptにおける新しい数値プリミティブで、任意精度の整数を表現することができます。BigInt
を使用すると、Number
の安全な整数制限を超える大きな整数を安全に保存し操作することができます。
BigInt
を作成するには、任意の整数リテラルにn
サフィックスを追加します。たとえば、123
は123n
になります。グローバルBigInt(number)
関数を使用して、Number
をBigInt
に変換することもできます。言い換えれば、BigInt(123) === 123n
です。これら2つの技術を使用して、以前の問題を解決しましょう。
BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// → 9_007_199_254_740_993n ✅
ここにもう1つの例があります。2つのNumber
を掛け合わせてみましょう。
1234567890123456789 * 123;
// → 151851850485185200000 ❌
最下位桁を確認すると、9
と3
があり、掛け算の結果は7
で終わるはずです(9 * 3 === 27
)。しかし、結果は0が並んで終わっています。それは正しくありません!代わりにBigInt
を使用して試してみましょう。
1234567890123456789n * 123n;
// → 151851850485185185047n ✅
今回は正しい結果が得られました。
Number
の安全な整数の制限はBigInt
には適用されません。そのため、BigInt
を使用すると、精度を失う心配をせずに正確な整数演算を実行できます。
新しいプリミティブ
BigInt
はJavaScript言語の新しいプリミティブです。そのため、typeof
演算子を使用して検出できる独自の型を持っています:
typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'
BigInt
が別の型であるため、BigInt
はNumber
と厳密に等しくなることはありません。たとえば、42n !== 42
です。BigInt
をNumber
と比較する場合は、比較を行う前に一方の型を他方に変換するか、抽象的等価演算子(==
)を使用してください:
42n === BigInt(42);
// → true
42n == 42;
// → true
論理値に強制型変換された場合(たとえば、if
、&&
、||
、またはBoolean(int)
を使用した場合)、BigInt
はNumber
と同じロジックに従います。
if (0n) {
console.log('if');
} else {
console.log('else');
}
// → 'else'がログ出力されます。なぜなら、`0n`は偽だからです。
演算子
BigInt
は最も一般的な演算子をサポートします。二項演算子の+
、-
、*
、および**
は期待どおりに動作します。/
と%
も動作し、必要に応じてゼロに向かって丸められます。ビット演算|
、&
、<<
、>>
、および^
は負の値に対して2の補数表現を仮定してビット演算を実行します。これはNumber
と同じです。
(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n
単項演算子-
は負のBigInt
値を表すために使用できます(例: -42n
)。単項演算子+
はサポートされていません。なぜなら、+x
は常にNumber
を生成するか例外をスローするべきであると期待するasm.jsコードを崩壊させるためです。
注意点として、BigInt
とNumber
の間で操作を混在させることは許可されていません。このルールは良いもので、暗黙の型変換によって情報が失われる可能性を防ぎます。以下の例を考えてみてください:
BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔
結果はどうなるべきでしょうか?良い答えはありません。BigInt
は小数を表現できず、Number
は安全な整数の範囲を超えたBigInt
を表現できません。そのため、BigInt
とNumber
の操作を混在させるとTypeError
例外がスローされます。
このルールの例外は、===
、<
、>=
などの比較演算子です。これらは真偽値を返すため、精度を失うリスクはありません。
1 + 1n;
// → TypeError
123 < 124n;
// → true
BigInt
とNumber
は一般に混在しないため、既存のコードをNumber
からBigInt
に“魔法のように”変換することを避けてください。どちらの分野で操作するかを決定し、その決定に固執してください。大きな整数に対して動作する新しいAPIに適しているのはBigInt
です。安全な整数範囲内であることが確認されている整数値には依然としてNumber
を使用するのが理にかなっています。
さらに注目すべき点として、符号なし右シフト演算子>>>
はBigInt
には意味がありません。なぜなら、BigInt
は常に符号付きだからです。このため、>>>
はBigInt
では動作しません。
API
いくつかの新しいBigInt
専用のAPIが利用可能です。
グローバルBigInt
コンストラクタはNumber
コンストラクタに似ています。それは引数をBigInt
に変換します(前述のとおり)。変換が失敗すると、SyntaxError
またはRangeError
例外がスローされます。
BigInt(123);
// → 123n
BigInt(1.5);
// → RangeError
BigInt('1.5');
// → SyntaxError
最初の例は数値リテラルをBigInt()
に渡しています。これは悪い慣習です。なぜなら、Number
は精度を失う可能性があり、BigInt
への変換が行われる前にすでに精度を失っている可能性があるからです:
BigInt(123456789123456789);
// → 123456789123456784n ❌
このため、BigInt
リテラル表記(n
サフィックス付き)を使用するか、BigInt()
に文字列(Number
ではありません!)を渡すことをお勧めします:
123456789123456789n;
// → 123456789123456789n ✅
BigInt('123456789123456789');
// → 123456789123456789n ✅
2つのライブラリ関数により、BigInt
値を特定のビット数に制限された符号付きまたは符号なし整数としてラップすることができます。BigInt.asIntN(width, value)
はBigInt
値を幅
桁の2進符号付き整数にラップし、BigInt.asUintN(width, value)
はBigInt
値を幅
桁の2進符号なし整数にラップします。たとえば、64ビット算術を行う場合、これらのAPIを使用して適切な範囲内に収めることができます:
// 符号付き64ビット整数として表現可能な
// 最高のBigInt値。
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
// → 9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ オーバーフローによって負になる
BigInt
値が64ビット整数範囲(つまり絶対数値のための63ビット + 符号のための1ビット)を超えるとすぐにオーバーフローが発生することに注意してください。
BigInt
により、他のプログラミング言語で一般的に使用される64ビット符号付きおよび符号なし整数を正確に表現することが可能になります。新しい型付き配列のフレーバーであるBigInt64Array
とBigUint64Array
により、そのような値のリストを効率的に表現し操作することが簡単になります。
const view = new BigInt64Array(4);
// → [0n, 0n, 0n, 0n]
view.length;
// → 4
view[0];
// → 0n
view[0] = 42n;
view[0];
// → 42n
BigInt64Array
のフレーバーは、その値が符号付き64ビットの範囲内にとどまることを保証します。
// 符号付き64ビット整数として表現可能な最大の`BigInt`値。
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
はChromeでのみサポートされています。他のブラウザも実装に向けて積極的に取り組んでいます。しかし、ブラウザ互換性を犠牲にせずにBigInt
の機能を今日使いたい場合はどうすればよいでしょうか?その答えは少なくとも興味深いものです。
他のほとんどのモダンなJavaScript機能とは異なり、BigInt
をES5にトランスパイルすることは合理的に不可能です。
BigInt
の提案には演算子の挙動を変更する(例えば+
、>=
など)ことが含まれます。この変更を直接ポリフィルすることは不可能であり、また、BigInt
コードをBabelなどのツールを使用してフォールバックコードにトランスパイルすることもほとんどの場合不可能です。その理由は、このようなトランスパイルはプログラム内のすべての演算子を入力の型チェックを行う関数への呼び出しに置き換えなければならず、これによりランタイムのパフォーマンスが許容できないほど低下するためです。さらに、トランスパイルされたバンドルのファイルサイズが大幅に増加し、ダウンロード、解析、コンパイル時間に悪影響を与えるでしょう。
現在は、より実行可能で未来志向の解決策としてJSBIライブラリを使用してコードを書くことです。JSBIはV8およびChromeのBigInt
実装をJavaScriptに移植したもので、設計上、ネイティブのBigInt
機能と完全に同じように動作します。その違いは、構文に依存する代わりにAPIを公開することです。
import JSBI from './jsbi.mjs';
const max = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
const two = JSBI.BigInt('2');
const result = JSBI.add(max, two);
console.log(result.toString());
// → '9007199254740993'
関心のあるすべてのブラウザがネイティブのBigInt
をサポートするようになったら、babel-plugin-transform-jsbi-to-bigint
を使用してコードをネイティブのBigInt
コードにトランスパイルし、JSBI依存を削除できます。例えば、上記の例は次のようにトランスパイルされます。
const max = BigInt(Number.MAX_SAFE_INTEGER);
const two = 2n;
const result = max + two;
console.log(result);
// → '9007199254740993'
さらなる学び
BigInt
が舞台裏でどのように動作するか(例えば、メモリ内での表現や演算の方法など)に興味がある場合は、実装の詳細を含むV8ブログ記事をご覧ください。