ほぼすべてを扱う、ひとつの数値型
多くの言語では整数型と浮動小数点型が別々に用意されていますが、JavaScript の場合は昔から Number ひとつだけでした。42 でも 3.14 でも -0.001 でも、返ってくるのは同じプリミティブ型 —— 64 ビットの IEEE 754 倍精度浮動小数点数です。
便利といえば便利です。int と float のキャストを気にする必要もないし、2^31 でオーバーフローすることもありません。ただ、内部的に浮動小数点で表現されているせいで、初心者が必ずと言っていいほどハマる落とし穴があります。そこで、Number では扱いきれないケースをカバーするために、2020 年に追加されたのが 2 つ目の数値型 BigInt です。
JavaScript の浮動小数点誤差に要注意
試しに次のコードを動かしてみてください。
1行目は 0.30000000000000004 と表示され、2行目は false になります。これはJavaScript特有のクセではなく、PythonでもJavaでもCでも、IEEE 754の浮動小数点数を使う言語ならどれでも同じ挙動になります。
原因はこうです。0.1 や 0.2 は2進数では正確に表現できません。10進数で 1/3 をきっちり書き下せないのと同じ話ですね。近似値が格納され、計算を重ねるたびに小さな誤差が積もっていきます。イメージとしては、小数を含む Number の値は「書いた数値にとても近い 近似値 」だと割り切るのが正解です。
お金を扱うときは、$19.99 を 19.99 のまま持ってはいけません。セント単位の整数 — つまり 1999 — として保存し、表示するときにフォーマットします。浮動小数点の誤差バグを避けるなら、まずこの習慣をつけるのが一番効きます。
浮動小数点数を安全に比較する
== や === での等値比較は当てにならないので、比較が必要な場面では許容誤差(トレランス)を使って判定します。
Number.EPSILON は、1 と「1 の次に表現可能な数」との差を表す最小値で、1 付近の値を比較するときの許容誤差として手頃なデフォルトです。絶対値がとても大きい/小さい場面では、入力値のスケールに合わせて誤差許容範囲を調整するのが定石です。
安全に扱える整数の範囲
ある程度の大きさまでの整数なら、64 ビット浮動小数点でも 正確に 表現できます。ただしその境界を越えると、1 ビットずつ精度がこぼれ落ちていきます。
2^53 - 1 は、「それ未満の整数がすべて正確に表現できる」最後の整数です。これを超えると、一部の整数は Number 型では表現できなくなり、最も近い値に丸められてしまいます。データベースから 64 ビットの ID を普通の JSON の数値としてパースしていると、気づかないうちにデータが壊れるバグの温床になります。
BigInt の登場
BigInt は任意精度の整数を扱うための独立したプリミティブ型です。整数リテラルの末尾に n を付けるか、BigInt(...) を呼び出すことで作成できます。
BigInt はメモリの許す限りいくらでも大きな整数を扱えます。具体的には、こんな場面で活躍します。
2^53を超えるデータベースIDや、Twitter/X のスノーフレークIDを扱うとき- 暗号計算をするとき
- 速度よりも正確な計算結果が求められる整数演算全般
逆に、普段使いのカウンタや配列のインデックス、金額をセント単位で扱うような処理には 向きません 。こういった用途では通常の Number の方が高速で、しかも言語内のあらゆるAPIとそのまま連携できます。
BigInt の算術演算
両辺が BigInt であれば、おなじみの演算子がそのまま使えます。
割り算はゼロ方向に切り捨てられます。小数の BigInt は存在しません。小数が必要になった場面では、Number に戻すか、decimal 系のライブラリを使うことになります。
型を混ぜてはいけない
よくハマるポイントがこれ。同じ式の中で Number と BigInt を混ぜて使うことはできません。
比較だけは例外で、< や >、== は型の境界をまたいで暗黙の型変換が行われます。
つまり == では等しいと判定されますが、=== では別物として扱われます。普段から === を徹底している方(そうすべきです)にとっては、Number と BigInt をまたいだ数値比較が出てきた時点で設計の違和感サインと捉えてください。どちらかの型に寄せてから比較するのが鉄則です。
Number と BigInt の変換
変換は2方向、それぞれに落とし穴があります。
Number → BigInt への変換は厳しめで、小数や NaN を渡すと例外になります。逆に BigInt → Number は緩いものの、情報が欠けます。MAX_SAFE_INTEGER を超えた値は丸められてしまうので注意してください。サーバーから受け取った BigInt を変換するときは、そもそも変換が本当に必要なのか一度立ち止まって考えるといいでしょう。
Number型の特殊な値
ついでに触れておくと、Number 型には数学的には「数」とは呼べない特殊な値が3つあります。
Infinity や -Infinity は、ゼロで割り算をしたときや、浮動小数点の表現範囲を超えてオーバーフローしたときに現れます。一方の NaN(not a number)は、計算しても意味のある結果が得られなかった場合に出てきます。
NaN には有名な性質があって、自分自身と等しくなりません。これは JS のバグではなく、IEEE 754 の仕様どおりの挙動です。判定には Number.isNaN(x) を使いましょう。古いグローバル関数の isNaN は、引数をまず数値に変換してから判定するため、isNaN("hello") が true になるなど誤った結果を返します。必ず Number.isNaN のほうを使ってください。
文字列から数値へのパース
ユーザー入力や JSON の数値フィールドは、文字列として渡ってくることがよくあります。変換方法は主に 3 つあります。
Number() は厳しめで、数値に変換できない文字列は全部 NaN になります。ただし、空文字列と空白だけは例外で 0 が返ります。一方で parseInt や parseFloat は寛容派で、読める範囲まで読んで途中で止めます。用途に応じて使い分けて、結果を使う前に NaN かどうかをチェックしましょう。
文字列から BigInt にパースしたいときは BigInt("123") を使ってください。こちらは厳密で、不正な入力だと例外を投げます。
ざっくりルールブック
- カウンタ、計算、座標など、日常的な数値は基本
NumberでOK。 - お金を扱うときは、整数のセント(円なら銭)単位にスケールして
Numberで扱うか、専用の decimal ライブラリを使う。 2^53を超える大きな整数(DBのID、暗号、組み合わせ計算など)は、nサフィックス付きのBigIntを使う。- 浮動小数点数の比較は
===ではなく、許容誤差(トレランス)を持たせて比較する。 - 不正な値のチェックは、グローバル版ではなく
Number.isNaNとNumber.isFiniteを使う。 - 同じ式の中で
NumberとBigIntを混ぜない。必要なら明示的に変換する。
次は null と undefined
JavaScript には「値がない」ことを表す方法が2つあります。null と undefined です。この2つ、似ているようで互換ではありません。次回は、それぞれが何を意味するのか、どう違うのか、そしてどちらをいつ使うべきかを見ていきましょう。
よくある質問
JavaScriptで 0.1 + 0.2 が 0.3 にならないのはなぜ?
JavaScriptの Number 型は64ビットのIEEE 754浮動小数点数で、0.1 や 0.2 は2進数で正確に表現できないからです。実際に計算すると 0.30000000000000004 になります。これはJavaScriptのバグではなく、同じ浮動小数点フォーマットを使うPythonやJavaでも起きる現象です。お金の計算をするなら、セント単位の整数に変換するか、decimal系のライブラリを使いましょう。
BigIntって何?どんなときに使えばいい?
BigInt は Number.MAX_SAFE_INTEGER(2^53 - 1)を超える整数を扱うための独立した数値プリミティブです。末尾に n を付けて 9007199254740993n と書くか、BigInt(value) で生成します。64ビットのデータベースID、暗号処理、精度が速度より重要な整数演算などで活躍します。
NumberとBigIntは混ぜて使える?
混ぜられません。1n + 1 を実行すると TypeError: Cannot mix BigInt and other types が発生します。BigInt(n) や Number(b) で明示的に変換しましょう。ただし < や == のような比較演算子は型をまたいで動作します。一方で === は型が違うので必ず false を返す点に注意してください。