この記事はQiita AdventCalendar JavaScript 2021 6日目 の記事です
2進数の世界で動いている汎用計算機が処理する数値は、整数か浮動小数点数の2つに分けられます。C言語の型ではintやdoubleです。それぞれの型はメモリ上でのビット長が決まっており、32bitや64bitです。
言語によっては数値の精度を必要ならいくらでも伸ばしたりできるような任意精度演算
という演算システム(実際上は利用可能なメモリ容量に制限される)による演算が可能な言語もあります。
ここではJavaScriptで数値がどのように扱われているのかを記述します。
JavaScriptの数値は1種類
JavaScriptにの数値型は1種類しかありません。
JavaScriptでは整数・浮動小数点数の区別がなくすべての数値が 64ビットの浮動小数(double型) のみです。
以下のようなプログラムでは一見整数を扱ってるように見えますが、これはすべて小数としてデータを扱ってることになります。
const x = 1
const y = 2
console.log(x + y) //3
double型でも52ビットをはみ出ない限り、整数は厳密に計算されます。
この出力は 3
と表示されますが、内部的には 3.0 === 3
と言うことになります。
IEEE 754 倍精度浮動小数点数
JavaScriptの数値型は IEEE 754 倍精度浮動小数点数 ( double )です。
これは64ビットの浮動小数点数表現の規格の一つです。doubleによる浮動小数点数の表現は広く使われており、コンピューターの小数表現のスタンダードです。
計算機の世界では有限個のデータしか扱えないので、プログラムで表す数値の精度には限界があります。
以下のよく知られた例を見てみましょう
console.log(0.1 + 0.2)
特に意識しなければ上記の例は 0.3
と出力されそうですが、実際には 0.30000000000000004
と出力されるはずです。
こういう例もある
console.log(0.3 === 0.1 + 0.2)
/* 出力:false */
なぜこのようなことになるかと言うと、0.1
や 0.2
という数が切りの悪い数なので正確に表現することができないのが原因です。
0.1
は2進数で表すと
0.00011001100...
と無限小数になり、double型で表現できる一番近い値に丸められます。
丸められた結果を10進数に戻すと
0.1000000000000000055511151231257827021181583404541015625
というような値になります。
0.2
も同様に丸められ10進数に戻すと以下のような値になります。
0.200000000000000011102230246251565404236316680908203125
0.1 + 0.2
の計算結果は以下の通りです。
0.3000000000000000444089209850062616169452667236328125
これは 0.1
、0.2
の丸められた誤差、 0.1 + 0.2
の加算で発生した丸めの誤差が計算結果に蓄積されているということです。これは、JavaScriptが実際に計算するのは 正確な 合計です。
プログラムで0.3
と書いた時点でもすでに誤差は発生しています。
コンピューターは実際の 0.3
に最も近いdouble型で表現な可能な数(↓)で解釈されます。
0.299999999999999988897769753748434595763683319091796875
これは 0.3
をdoubleで表現しようとすると負の方向に誤差が発生しますが、 0.1
と 0.2
ともに正の誤差を持っており、これらが蓄積した 0.1 + 0.2
は 0.3
から離れるため、0.3
( double型で表現可能な 0.3
に最も近い値)からずれてしまいます。このズレはビット表現ではわずか1ビット。0.3
にちょうど1ビットです。これは 0.3
にちょうど1ビット分の値である $2^{-54}$ を足すと0.1 + 0.2になります。
// これはtrueと出力される
console.log(0.3 + 2 ** (-54) === 0.1 + 0.2)
ここで言いたいことは、このような挙動はJavaScriptに関わらず 浮動小数点数(IEE754)の仕様 であり、
コンピューターで小数を表現しようとする限り避けようのないものであると言うことです。
JavaScriptの数値精度は52ビット
JavaScriptの数値は整数・浮動小数点数で区別がないことは冒頭で説明しましたが、
これは 整数だろうと小数だろうとすべてdoubleで表現している ということです。
つまり JavaScriptの整数の精度も52ビット ということになります。
整数と小数を区別する他のプログラミング言語では整数の精度は32ビットだったり64ビットだったりするので、それに比べるとかなり中途半端に感じると思います。ただそれらの言語と比べるとJavaScriptでの整数はdoubleであることに由来する 下の桁から大雑把になっていく という挙動をすることがあります。
整数の精度が52ビットということは、 $0$から$2^{53}-1$ までの整数は正確に解釈できることを意味します。これで重要なのは、ここで正確に表現できるというのは 1つ違う隣の整数と区別できる 事を意味します。以下の例は$2^{53}-1$が両隣の数と区別できることを表しています。
console.log(2 ** 53 - 2 === 2 ** 53 -1)
// faise (2 ** 53 - 2 === 2 ** 53 -1は違うもとの解釈される
console.log(2 ** 53 - 1 == 2 ** 53)
// false (2 ** 53 - 1 == 2 ** 53は違うもと解釈される
NaN と Infinity、+0 と- 0
JavaScriptには NaN
と Infinity
という特別な数値が存在します。 NaN
はNot a Numberの略で有ることは知られていますが、JavaScriptでは数値の一種なので型を調べると以下のように数値型となります。
console.log(typeof NaN)
// number
0 / 0
という計算をするとNaNとなります。
NaNの面白い挙動は NaN === NaN
とするとfalseとなるところです。 NaN < NaN
等のNaN含む比較演算子は全部falseになります。ある数値をNaNか判定したい場合は isNaN
を使うといいでしょう。
Infinity
も同様で、これは「無限大」を表現する特別な数値で、IEEE 754由来です。無限大には正の無限大、負の無限大があります。
doubleには +0
と -0
という2種類の0が存在し、JavaScriptでも2種類の0が確認できます。(通常の0は正の +0
となります)。+0
と -0
を === で比較してもtrueとなるのが特徴です。
このようなJavaScriptにおいて使われている等値比較演算子である ===
もIEEE 754の影響をうけています。
ES2015以降では、IEEE 754の影響を排除した等値比較の手段として Object.is
という関数が用意されてるようです
まとめ
ここではJavaScriptの数値型について書いてみました。
ポイントはJavaScriptの数値型は整数と浮動小数点数を区別せず、
IEEE 754倍精度浮動小数点数 で表す点です。
その結果整数が52ビットの精度をもっており、浮動小数点数特有の 0.3 !== 0.1 + 0.2
のような挙動が現れます。どの挙動がJavaScript特有の物でどの挙動がIEEE 754由来の話なのかをよく考えてコーディングするといいでしょう。