https://qiita.com/uhyo/items/f9abb94bcc0374d7ed23
数値というのはプログラミングにおいて極めて基本的な対象です。ほとんどのプログラミング言語は何らかの形で数値の操作を行うことができ、もちろんJavaScriptにおいても例外ではありません。
プログラミングにおける数値の特徴的な点は、往々にしてその性質に応じた複数の型1が与えられている点です。まず、数値は整数か小数かによって分類されます。さらに、値を表すのに使われるビット数、また整数に関しては符号ありか符号なしかという分類ができます。例えば、Rustという言語ではこれらの分類が分かりやすく表れています2。Rustにおける数値の型はi32
, i64
, u32
, u64
, f32
, f64
などがあり、見ただけでどのような特徴を持つ数値なのかが分かりやすくなっています。i
というのは符号あり整数、u
というのは符号なし整数、f
は小数で、その後の数字がビット数ですね。
では、JavaScriptにおいては数値はどのように扱われているのでしょうか。この記事では、JavaScriptの数値はどのように表されるのか、計算はどのように行われるのかなど、JavaScriptの数値に関するトピックを網羅します。
実は、JavaScriptの数値は型が1種類しかありません。熱心な方はBigInt
の存在についてご存知かと思いますが、まだこれはぎりぎりJavaScriptに入っていないのでここでは1種類とカウントします。(BigIntについてはこの記事の後半で触れます。)
1種類しかないということは、上で紹介した「整数か小数か」「ビット数」などによる区別を持たないということです。結論から言ってしまえば、JavaScriptの数値は64ビットの浮動小数点数です。つまり、JavaScriptの数値は全てが小数なのです。
以下のようなプログラムではJavaScriptで整数を扱っているように見えますが、実はこれも1.0
や2.0
などのデータを扱っていることになります(整数と小数を区別する言語では1.0
のように小数点を明示することでそれが小数データであることを明示することがあり、この記事でもそれに倣っています)。
const x = 1;
const y = 2;
console.log(x + y); // 3
3
などと表示されるのも処理系が気を利かせているのであり、内部的には3.0
というデータになっています3。
より詳細に言えば、JavaScriptの数値型はIEEE 754 倍精度浮動小数点数です(長いので以降はdoubleと呼ぶことにします)。これは何かというと、64ビットの範囲内でどのように小数を表現するかを定めた規格のひとつです。doubleによる小数(浮動小数点数)の表現は極めて広く使用されており、コンピュータにおける小数の表現のスタンダードとなっています。
まず前提として、64ビットという限られたデータ量で任意の小数を表すのは不可能です。それゆえ、プログラムで表せる数値の精度には限りがあります。このことは、以下のよく知られた例に表れています。
// 0.30000000000000004 と表示される
console.log(0.1 + 0.2);
// false と表示される
console.log(0.3 === 0.1 + 0.2);
これは、0.1
とか0.2
という数が(2進数で数値を扱うコンピュータにとっては)きりの悪い数なので正確に表すことができないことが原因です。なので、0.1
と書いた時点で、コンピュータはそれをぴったり0.1
という数ではなく0.1000000000000000055511151231257827021181583404541015625
という数として認識します。64ビットという限られたデータ量ではこれが精一杯で、これ以上正確に0.1
を表すことはできないのです(doubleという規格を用いるならばの話ですが)。0.2
についても同様で、コンピュータはこれを0.200000000000000011102230246251565404236316680908203125
と認識します。この時点で、0.1
も0.2
もともに、実際よりもわずかに大きいほうに誤差が発生しています。
0.1 + 0.2
という計算の結果は0.3000000000000000444089209850062616169452667236328125
です4。ポイントは、0.1
や0.2
と書いた時点で発生していた誤差、そして加算で発生した丸め誤差がこの計算結果に蓄積しているということです。
一方で、プログラムに0.3
と書いた場合もやはりすでに誤差が発生しており、コンピュータは実際の0.3
に最も近いdoubleで表現可能な数である0.299999999999999988897769753748434595763683319091796875
を採用します。
ここで運悪く、0.3
をdoubleで表現しようとすると負の方向に誤差が発生しています。0.1
と0.2
はともに正の誤差を持っていたこともあり、これらが蓄積した結果0.1 + 0.2
は0.3
から離れすぎてしまいます。その結果、0.1 + 0.2
の結果は0.3
(に最も近いdoubleで表現可能な数)からずれてしまうのです。このずれはビット表現でいうとわずか1ビットです。実際、0.3
にちょうど1ビットぶんの値であるを足すと0.1 + 0.2
になります。
// true と表示される
console.log(0.3 + 2 ** (-54) === 0.1 + 0.2);