Edited at

JavaScriptの数値型完全理解

数値というのはプログラミングにおいて極めて基本的な対象です。ほとんどのプログラミング言語は何らかの形で数値の操作を行うことができ、もちろんJavaScriptにおいても例外ではありません。

プログラミングにおける数値の特徴的な点は、往々にしてその性質に応じた複数の1が与えられている点です。まず、数値は整数小数かによって分類されます。さらに、値を表すのに使われるビット数、また整数に関しては符号あり符号なしかという分類ができます。例えば、Rustという言語ではこれらの分類が分かりやすく表れています2。Rustにおける数値の型はi32, i64, u32, u64, f32, f64などがあり、見ただけでどのような特徴を持つ数値なのかが分かりやすくなっています。iというのは符号あり整数、uというのは符号なし整数、fは小数で、その後の数字がビット数ですね。

では、JavaScriptにおいては数値はどのように扱われているのでしょうか。この記事では、JavaScriptの数値はどのように表されるのか、計算はどのように行われるのかなど、JavaScriptの数値に関するトピックを網羅します。


JavaScriptの数値は(今のところ)1種類

実は、JavaScriptの数値は型が1種類しかありません。熱心な方はBigIntの存在についてご存知かと思いますが、まだこれはぎりぎりJavaScriptに入っていないのでここでは1種類とカウントします。(BigIntについてはこの記事の後半で触れます。)

1種類しかないということは、上で紹介した「整数か小数か」「ビット数」などによる区別を持たないということです。結論から言ってしまえば、JavaScriptの数値は64ビットの浮動小数点数です。つまり、JavaScriptの数値は全てが小数なのです。

以下のようなプログラムではJavaScriptで整数を扱っているように見えますが、実はこれも1.02.0などのデータを扱っていることになります(整数と小数を区別する言語では1.0のように小数点を明示することでそれが小数データであることを明示することがあり、この記事でもそれに倣っています)。

const x = 1;

const y = 2;
console.log(x + y); // 3

3などと表示されるのも処理系が気を利かせているのであり、内部的には3.0というデータになっています3


IEEE 754 倍精度浮動小数点数 (double)

より詳細に言えば、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.10.2もともに、実際よりもわずかに大きいほうに誤差が発生しています。

0.1 + 0.2という計算の結果は0.3000000000000000444089209850062616169452667236328125です4。ポイントは、0.10.2と書いた時点で発生していた誤差、そして加算で発生した丸め誤差がこの計算結果に蓄積しているということです。

一方で、プログラムに0.3と書いた場合もやはりすでに誤差が発生しており、コンピュータは実際の0.3に最も近いdoubleで表現可能な数である0.299999999999999988897769753748434595763683319091796875を採用します。

ここで運悪く、0.3をdoubleで表現しようとすると負の方向に誤差が発生しています。0.10.2はともに正の誤差を持っていたこともあり、これらが蓄積した結果0.1 + 0.20.3から離れすぎてしまいます。その結果、0.1 + 0.2の結果は0.3(に最も近いdoubleで表現可能な数)からずれてしまうのです。このずれはビット表現でいうとわずか1ビットです。実際、0.3にちょうど1ビットぶんの値である$2^{-54}$を足すと0.1 + 0.2になります。

// true と表示される

console.log(0.3 + 2 ** (-54) === 0.1 + 0.2);

結局ここで何が言いたいかというと、このような挙動はJavaScriptとは関係のない浮動小数点数の仕様であり、コンピュータで(固定長のデータで)小数を表そうとする限り避けようのないものであるということです。

ゆえに、JavaScriptの数値型を理解するには、IEEE 754由来の挙動はどれかということも知らなければなりません。というわけで、もう少しだけIEEE 754(double)について見てみましょう。


doubleによる小数の表現では、64ビットのうち最初の1ビットを符号ビット(0なら正の数、1なら負の数を表す)、次の11ビットを指数部、残りの52ビットを仮数部として用います。本質的には、指数部と仮数部はそれぞれ2進数で表された整数であると解釈されます。仮数部が$x$という整数で指数部が$y$という整数であるとき、その小数表現が表すのは$x \times 2^y$となります(実際には$x$はいわゆるけち表現を加味して決められることや、$y$は正負をバランスよく表せるように調整されることに注意が必要です)。ここから分かることは、指数部のビット数というのは小数で表せるスケール(どれくらい大きな/小さな範囲の数を表せるか)、そして仮数部のビット数というのは小数で表せる精度(小数を表すのにどれくらいの桁数を使えるか)に関わるということです。

特に、doubleにおける仮数部の精度は53ビットであることは覚えておくに値するでしょう(52ビットからなぜ1つ増えているのかは調べてみてください)。

そろそろ話をJavaScriptに戻しますが、JavaScriptの数値がdoubleによって表されているという話と上記のdoubleの仕様を組み合わせて言えることは、JavaScriptの数値の精度は53ビット分であるということです。


JavaScriptの数値の精度は53ビット

JavaScriptは整数と小数という区別が無いということは冒頭でお話ししましたね。これは、繰り返しになりますが、整数だろうと小数だろうと全部doubleで表しているということです。

ということはJavaScriptの整数の精度も53ビットということです。整数と小数を区別するプログラミング言語では整数の精度は32ビットだったり64ビットだったりしますから、それに比べると随分中途半端に思えます。ただ、それらの言語と比べると、JavaScriptでの整数はdoubleであることに由来する面白い挙動をします。それはだんだん下の桁から大ざっぱになっていくという挙動です。まずこの辺りを見ていきましょう。

整数の精度が53ビットということは、$0$から$2^{53}-1$までの整数は正確に表せるということを意味します。ただし、ここで正確に表せるというのは1つ違うとなりの整数と区別できるということを意味します。次の例は、$2^{53}-1$が両隣の数と区別できることを確かめています。

// false と表示される(2^53 - 2 と 2^53 - 1は違うものと認識されている)

console.log(2 ** 53 - 2 === 2 ** 53 - 1);
// false と表示される(2^53 - 1 と 2^53は違うものと認識されている)
console.log(2 ** 53 - 1 === 2 ** 53);

この範囲を超えると、となりの整数と区別することができなくなります。例えば、$2^{53}$と$2^{53}+1$は区別することができません。両者をdoubleで表すと同一のビット列となるためです。

// true と表示される(2^53 と 2^53 + 1は同じと認識されている)

console.log(2 ** 53 === 2 ** 53 + 1);

$2^{53}$を2進数で表すと1000000000000000000000000000000000000000000000000000001の後に0が53個)である一方、$2^{53}+1$は100000000000000000000000000000000000000000000000000001です。どちらも整数としての表現に54ビット必要としていることが分かります。doubleは数の精度を53ビットしか確保できませんから、53ビットになるように丸められます(このときの丸めモードは最近接(偶数)丸めです)。これにより、両者はどちらも$2^{53}$に丸められて等しいと扱われるのです。

一方、$2^{53}+2$という数を考えてみます。これは100000000000000000000000000000000000000000000000000010ですから、53ビットに情報が減らされた後も依然として$2^{53}$とは異なります。

console.log(2 ** 53 === 2 ** 53 + 2); // false

この例では、整数として表すのに54ビット必要になったことで整数の精度が1ビット分大ざっぱになりました。さらに数を大きくしていくことで、下のほうからどんどんビットが削られて大ざっぱになっていくわけです。

このような事情から、JavaScriptにはsafe integer(安全な整数?)という概念があります。これは絶対値が$2^{53}-1$以下の整数を指し、計算結果がsafe integerならばずれが発生していない(上述の現象により正しい答えを表す整数からずれていない)ことが分かります5

JavaScriptには与えられた数がsafe integerかどうか判定するNumber.isSafeInteger関数が用意されています。

console.log(Number.isSafeInteger(2 ** 53 - 1)); // true

console.log(Number.isSafeInteger(2 ** 53)); // false

こう真面目に解説するとJavaScriptはアホな言語なのではないかと思われるかもしれませんが(そして整数もdoubleで表さないといけないのが実際アホなことは否定しませんが)、整数を浮動小数点数で表そうとしたときにこのように情報が落ちていくことはdoubleの仕様から来る話でJavaScriptに特有の話ではないということは理解しておくべきでしょう。


NaNInfinity, +0-0

JavaScriptにはNaNInfinityという特別な数値が存在します。NaNはNot a Number(数ではない)の略であることは知られていますが、JavaScriptでは数値の一種なので型を調べると数値型です。

console.log(typeof NaN); // "number"

言うまでもなく、これもIEEE 754由来の概念です。ですから、「JavaScriptにはNaNとかいう意味不明な値がある」などとは思わないでくださいね。

ただ、doubleはNaNを表すビットパターンを大量に($2^{53}-2$個くらい)持ちますが、JavaScriptにおいてはNaNはただ一種類であり区別はありません。

NaNは数値が必要だけどどうしても無理な場合に表れます。例えば、parseInt(後述)で文字列を数値に変換しようとしたけど無理だった場合はNaNが結果となります。

console.log(parseInt("foobar!")); // NaN

また、0 / 0という計算をした場合もNaNとなります。

NaNの面白い(もちろんIEEE 754由来の)点は、NaN === NaNという比較がfalseとなることです。NaN < NaNなど、NaNを含む比較演算はすべてfalseとなります。ある数値がNaNかどうか判定した場合はisNaN(後述)を使うのがよいでしょう。

Infinityも同様です。これは「無限大」を表す特別な数値であり、IEEE 754由来です。無限大には正の無限大と負の無限大があります。

さらに、doubleは+0-0という2種類の0が存在し、JavaScriptにおいても2種類の0が確認できます(普通の0は+0です)。+0-0===で比較しても等しいのが特徴です。

このように、JavaScriptにおいて広く使われる等値比較演算子である===もIEEE 754の影響を受けています。ES2015以降では、IEEE 754の影響を排除した等値比較の手段としてObject.isという関数が用意されています(cf. JavaScriptの等値比較を全部理解する)。

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

console.log(Object.is(0, -0)); // false

以上でJavaScriptの数値型が裏ではどのようになっているか分かりました。これを踏まえてJavaScriptにおける数値に関連する演算を見ていきたいのですが、その前に数値リテラルの話を挟みます。


JavaScriptの数値リテラル

数値リテラルとは、プログラム中で数値を表現する方法です。プログラム中に123とか0.45と書いたら当然123とか0.45という数値になりますが、これらが数値リテラルです。

これはJavaScriptに限った話ではありませんが、0.123のように0.で始まる小数を書きたい場合は最初の0を省略して.123のように書けます。見かけても驚かないようにしましょう。

console.log(.123); // 0.123

また、整数を書きたい場合は123.0の最後の0を省略して123.とできます。JavaScriptではただ123とすればいいので特に意味はありませんが、整数と浮動小数点数を区別する言語では123123.0が別の意味になるために後者を省略したい場合の記法として需要があるようです。

なお、数値のメソッドを呼びたい場合には注意してください。123toFixedメソッド(後述)を呼びたい場合には、123.toFixed()とするとエラーとなります。なぜなら、123.という数値の直後にtoFixedが並んでいる(123 toFixed()と書いたのと同じ)と解釈されるからです。これを回避するひとつの方法は(123).toFixed()ですが、文字数の少なさと見た目の面白さからこれを123..toFixed()とするのをよく見ます。こうすることで、123.が数値、.toFixed()でメソッド呼び出しと解釈されます。もちろん、123.0.toFixed()なども動作します。

さらに、数値リテラルは指数表記をサポートしています。これは1.23e5のように整数または小数のうしろにeと整数を付けるリテラルです。eの後ろは10の指数と解釈されます。よって、1.23e5は$1.23 \times 10^5$、すなわち123000と解釈されます。1e-40.0001と同じ)のようにeの後ろは負の整数も可能です。

また、ここまでは10進数表記でしたが、他に16進数・8進数・2進数のリテラルがサポートされています。それぞれ0xabcdef0o7550b1010111のようなリテラルです。これらの10進数以外のリテラルは小数点やeによる指数表記はサポートされていません。

また、桁数の多い数値リテラルを見やすく1_234_567のように書ける提案がもう少しで完成しそうです(cf. JavaScriptで数値の区切り文字を使いたい物語)。

以上が数値リテラルの話でした。まあ、特におかしな所はありませんでしたね。では、いよいよ数値演算の話に入っていきます。


JavaScriptにおける数値演算

とはいえ、JavaScriptの数値演算は、それほど特筆すべき点があるわけではありません。まず、普通の四則演算(+, -, *, /及び余り%)と累乗(**)が備えられています。

ただし、+は文字列の連結にも使われるので"5" + 1"51"になったりする点はやや注意が必要でしょうか。-とかは両辺を数値に変換するので"5" - 14です。これらの演算子にオブジェクトを渡してしまったときの挙動はちょっと面白かったりするのですが(cf. JavaScriptのプリミティブへの変換を完全に理解する)、いまは関係のない話です。

やや注意が必要なのはビット演算です。JavaScriptは一般的なビットごと演算(&, |, ~)やビットシフト(<<, >>, >>>)を備えていますが、これらを使う場合は突然数値が32ビット整数に変換されます。このとき小数は0に近いほうへ丸められます。この結果として、32ビットを超える範囲の整数は下位32ビットのみが残されて他は捨てられます。また、負数は普通の2の補数表現によって表されます。

そして、32ビット整数に対してビット演算が適用され、結果が32ビット符号あり整数として再解釈されます(ただし、>>>のみ32ビット符号なし整数として解釈します)。

以上の説明で以下の結果が理解できることでしょう。

console.log((2 ** 32) | 0);   // 0

console.log(2 ** 31); // 2147483648
console.log((2 ** 31) | 0); // -2147483648
console.log((2 ** 32) >> 0); // 0
console.log((2 ** 31) >> 0); // -2147483648
console.log((2 ** 31) >>> 0); // 2147483648

また、<<などのビットシフト演算の右オペランド(シフト量を指定する数)についても右側は32ビット整数として扱われます。また、対象が32ビット整数ということで32個以上シフトするのは意味がないと考えられることから、32以上の数については32で割った余りが取られます。よって、次のような結果となります。

console.log(1 << 33);   // 2

console.log(1 << (-1)); // -2147483648


数値から文字列への変換

数値の計算に関する話は終わりにして、数値を文字列に変換したいときの話をしましょう。実は、JavaScriptは数値から文字列への変換メソッドを5種類提供しています。いずれも数値が持つメソッドとして利用可能です。


toExponential

このメソッドは数値を指数表現で文字列に変換します。指数表現というのは1.23e+5($1.23 \times 10^5$を表す)のようにeを用いた表現です。toExponentialはどのような数値でもこの表現に変換します(NaNとかInfinityは除く)。引数で、小数点以下の桁数を指定できます。省略すると数値を表現するのに最低限必要な分を適切に選んでくれます。

console.log(150..toExponential(3)); // "1.500e+2"

console.log(0.1234.toExponential(1)); // "1.2e-1"


toFixed

toFixedは、逆に指数表現を用いない文字列表現を得たいときに使います。引数は小数点以下の桁数で、省略の場合は0(整数部分のみ)扱いです。また、絶対値が$10^{21}$以上の数は諦めてしまいます(後述のtoStringと同じになります)。

console.log(100..toFixed(3));     // "100.000"

console.log(0.000999.toFixed(5)); // "0.00100"

特筆すべき点として、safe integerの範囲を超える(しかし$10^{20}$を超えない)範囲の整数に対してはtoString(後述)よりもtoFixedのほうがより正確な610進表現を求めてくれます。小数についても同様の場合があります。

console.log((2 ** 60).toString());   // "1152921504606847000"

console.log((2 ** 60).toFixed()); // "1152921504606846976"


toPrecision

toPrecisionは、引数で渡した数を有効数字の桁数とする文字列表現に直してくれます。必要に応じて指数表現が使われます。

console.log(1.234.toPrecision(3));        // "1.23"

console.log(1234..toPrecision(3)); // "1.23e+3"
console.log(0.00123.toPrecision(2)); // "0.0012"
console.log(0.0000000123.toPrecision(2)); // "1.2e-8"


toString

toStringは数値から文字列への暗黙の変換の場合に使われる標準的な方法です。結果は数値を普通に(必要最小限の桁数で)表現したものですが、toPrecisionの場合と同様に必要に応じて指数表現も使われます。

toStringには大きな特徴が2つあります。1つ目は、10進表示への変換を適度にサボることです。toFixedの例を再掲します。2 ** 60($2^{60}$)に対する結果がtoStringtoFixedで違っているという例でしたね。

console.log((2 ** 60).toString());   // "1152921504606847000"

console.log((2 ** 60).toFixed()); // "1152921504606846976"

$2^{60}$は正確に1152921504606846976ですが、(2 ** 60).toString()は最後の4桁が7000と大ざっぱになっています。

しかし、JavaScriptの整数の精度が53ビットしかないことを考えれば、実は11529215046068470002 ** 60になることが分かります。

console.log(2 ** 60 === 1152921504606846976); // true

console.log(2 ** 60 === 1152921504606847000); // true

$2^{60}$という61ビットの数が53ビットに情報を減らされる場合、8ビット分情報が落ちます。こうなると、丸めのことを考えてもその半分(7ビット)程度の違いは意味を成さなくなります。$2^7$は128ですから、69767000の間のたった24の差は無視できるのです。

このように、toStringは無視できる範囲で情報を落としてもよいことになっています。ちなみに、小数の場合でもこれは同じです。次の例を見れば分かるように、これはわりと人間に優しい仕様であるといえますね。

console.log(0.3.toString());  // "0.3"

console.log(0.3.toFixed(20)); // "0.29999999999999998890"

この場合、わざわざ0.2999999999999999889と書いても0.3と書いても結果は同じであるため、より少ない桁数で表されるほうが選ばれています。

console.log(0.2999999999999999889 === 0.3); // true

toStringにはもうひとつ特徴があります。それは、引数で基数を指定できる点です。これにより、10進数以外の表現を得ることができます。

console.log(12345.67.toString(16)); // "3039.ab851eb852" (Google Chrome 74の場合)

ちなみに、10進数以外は文字列を生成する具体的なアルゴリズムは実装依存です。


toLocaleString

これはIntl APIの一部で、言語や地域等の慣習に合わせて数値を文字列に変換してくれるメソッドです。詳しく説明するのは別の機会に譲るとして、ひとつだけ例を出しておきます。

console.log(1234567..toLocaleString("ja-JP", { style: "currency", currency: "JPY"}); // "¥1,234,567"


文字列から数値への変換

逆に、文字列をいい感じに解釈して数値に変換するという機能にも需要がありますよね。JavaScriptにはそのための方法がいくつかあります。


parseInt

その一つはparseIntです。Intというのはinteger(整数)のことですから、これは整数を表す文字列を数値に変換してくれます。

parseIntは大きな特徴が2つあり、1つは数値の後ろの余計な文字列を無視してくれることです。数値として解釈できない文字列はNaNとなります。

console.log(parseInt("55000"));   // 55000

console.log(parseInt("123px")); // pxが無視されて結果は 123
console.log(parseInt("123.45")); // 小数はパースしないので.45が無視されて 123
console.log(parseInt("foobar")); // NaN

もうひとつは、第2引数で基数を指定できる点です。2から36まで可能です。

console.log(parseInt("ff", 16));        // abcdef を16進数で解釈すると 255

console.log(parseInt("100zzzzzz", 16)); // 256

ちなみに、16進数の場合はparseIntは特殊な挙動をします。それは、0xで始まる文字列をいい感じに解釈してくれるというものです。とてもいい迷惑ありがたい機能ですね。

console.log(parseInt("0xff", 16)); // 255

では、parseIntの第2引数を省略した場合はどうなるのでしょうか。この場合は先の例からも分かるように10進数として扱われますが、省略した場合はひとつ特殊な挙動があります。それは0xで始まる文字列を渡された場合で、この場合だけ気を利かせて16進数として解釈してくれるのです。

console.log(parseInt("0xff"));     // 255

console.log(parseInt("0xff", 10)); // 0 (第2引数を指定するとこの挙動はオフになるため)

最後に、parseIntは文字列前後の空白文字を無視してくれます。

console.log(parseInt("      123   ")); // 123

console.log(parseInt(" 1 2 3 ")); // 1 (1の後の空白以降は全部余計な文字と見なされるので)

ちなみに、グローバル変数のparseIntではなくNumber.parseIntもあります(ES2015で追加)。こちらも同じ挙動となります。


parseFloat

parseFloatは、整数だけでなく小数も解釈してくれる関数です。まず、文字列前後の空白文字を無視してくれたり、後ろに余計なものが付いていても無視してくれるという点はparseIntと同じです。

parseFloatについては、10進の数値リテラルを文字列として表したものを解釈できます。

console.log(parseFloat("    123.45px")); // 123.45

console.log(parseFloat("1.23e4")); // 12300
console.log(parseFloat(" .456")); // 0.456

16進など、他の基数のリテラルは無理です。

console.log(parseFloat("0x123")); // 0

加えて、parseFloatInfinityに対する特別なサポートがあります。

console.log(parseFloat("Infinity")); // Infinity

Infinityと書くとInfinityという数が得られますがこれはInfinityというグローバル変数が存在するからこそなので、parseFloat"Infinity"に対応しているのはなかなか面白いですね。

実に簡単ですね。なお、Number.parseFloatをグローバルのparseFloatと同様に使えるのもparseIntと同じです。


Number

Numberは渡したものをなんでも数値に変換してくれる関数です。また、+"123"のように数値への暗黙の変換が要求される場合もこの変換が使われます。

Numberは、parse系メソッドと同様に前後に空白文字があっても許容します。ただし、それ以外の余計な文字が後ろにくっついているのはNaNとなります。

console.log(Number("1234"));     // 1234

console.log(Number(" 1234 ")); // 1234
console.log(Number("1234px")); // NaN

この点を除いて、Numberは全ての数値リテラルを受け付けます7。また、parseFloatと同様にInfinityも受け付けます。

すなわち、parseFloatが受け付けてくれた全てのリテラルに加えて"0xff"0b10101なども受け付けられるということです。parseIntと挙動が揃っていないのはご愛嬌です。

console.log(Number("0xff"));  // 255

console.log(Number("0b101")); // 5


その他の数値関係メソッド

他にもいくつか数値関係のメソッドがあるので紹介します。


isNaNisFinite

isNaNは引数で与えられた数がNaNかどうか判定するメソッドです。

console.log(isNaN(123));      // false

console.log(isNaN(Infinity)); // false
console.log(isNaN(NaN)); // true

NaNNaN === NaNfalseになってしまうため、isNaNNaNかどうか判定する簡単な方法です。

また、isFiniteは与えられた数がNaNInfinityまたは-Infifityだったらfalseで他はtrueを返すメソッドです。これは意外と使いどころがある関数です。

console.log(isFinite(123.45));   // true

console.log(isFinite(NaN)); // false
console.log(isFinite(Infinity)); // false

parseIntなどと同様にこれらにもNumberの下にあるバージョン、すなわちNumber.isNaNNumber.isFiniteがありますが、何とこれらはisNaNisFiniteとは微妙に挙動が違います

Numberバージョン、すなわちNumber.isNaNNumber.isFiniteは、与えられたものが数値でない場合は即座にfalseを返します。一方、グローバルのisNaNisFiniteはまず与えられたものを(Numberで)数値に変換しようとします。その結果、以下のような違いが現れることになります。

console.log(isNaN("foobar"));        // true ("foobar"を数値に変換したらNaNなので)

console.log(Number.isNaN("foobar")); // false ("foobar"は数値ではないので)
console.log(isFinite("1234")); // true
console.log(Number.isFinite("1234"));// false


Number.isInteger, Number.isSafeInteger

Number.isIntegerは単純ですね。与えられた数値が整数かどうかを判定します。Number.isSafeIntegerは少し前に話題にのぼりました。絶対値が$2^{53}-1$以下の整数に対してtrueを返します。これらはNumber.isFiniteなどと同様に、数値以外には即falseを返します。

console.log(Number.isInteger(1234));     // true

console.log(Number.isInteger(123.4)); // false
console.log(Number.isInteger(Infinity)); // false


Numberの定数

数値に関連するメソッドは以上ですが、Numberにはいくつか付随する定数があります。

Number.MAX_SAFE_INTEGERは$2^{53}-1$です。つまり最大のsafe integerですね。同様に、Number.MIN_SAFE_INTEGERは$-(2^{53}-1)$です。

Number.MAX_VALUEはJavaScriptの数値型で(すなわちdoubleで)表現可能な最大の数です(Infinityは除く)。

console.log(Number.MAX_VALUE); // 1.7976931348623157e+308

これは約$1.7976931348623157 \times 10^{308}$のようですね。

同様に、Number.MIN_VALUEはdoubleで表現可能な最小の正の数です。

console.log(Number.MIN_VALUE); // 5e-324

Number.MAX_VALUEに比べると有効数字が少ないような気がしますが、それはdoubleの0に近い部分では非正規化数(精度を落とすことで通常の小数(正規化数)よりも絶対値が0に近い数を表現する仕組み)が現れるからです。実際、この5e-324という数は精度を1ビットまで落とすことでぎりぎりまで0に近づけた数です。0より大きく5e-324より小さい数はJavaScript(あるいはdouble)には存在しません。例えば、5e-324 / 20となります。

他にはNumber.POSITIVE_INFINITYInfinityが入っている)とNumber.NEGATIVE_INFINITY-Infinityが入っている)、そしてNumber.NaNがあります(NaNが入っている)があります。グローバル変数のNaNInfinityが信用ならないときに使いましょう。

最後に、Number.EPSILONという定数もあります。これは、「1」と「(doubleで表せる)1よりも大きい最小の数」の差です。doubleの仕組みを理解していれば、これが$2^{-52}$であることはすぐに分かるでしょう。

console.log(Number.EPSILON === 2 ** (-52)); // true

以上で、数値に関する話はだいたい語りつくしました。他にはMathオブジェクト以下で提供されるさまざまな数学関数もあります(ES2015でいろいろ追加されたのでまだ調べていない人は調べてみてください)。ここでは全ては紹介しませんが、少しだけ触れておきます。

面白いところでは、Math.clz32という関数があります。これは、与えられた数を32ビット(符号なし)整数に変換してleading zeroesを数える(2進表現における一番左の1よりも左にある0の数を数える)関数です。

console.log(Math.clz32(1)); // 31

console.log(Math.clz32(2 ** 31)); // 0
console.log(Math.clz32(-(2 ** 31))); // 0

また、Math.imulは2つの引数を符号なし32ビット整数に変換したあと乗算を行い、結果の下部32ビット(を符号あり32ビット整数として解釈したもの)を返します。

console.log(Math.imul(2 ** 15, 2 ** 16)); // -2147483648

他は普通の数学関数なので調べてみてください。


BigIntの話

さて、以上で数値の話は終わりました。しかし、この記事はこれだけでは終わりません。JavaScriptにはBigIntがあります。これはまだJavaScriptに正式採用されていないものの現在Stage 3のプロポーザルです。これは要するにJavaScriptにもうそろそろ正式に追加されそうということで、恐らく2020年にリリースされるES2020で正式にJavaScriptの仕様に追加されると思います。

BigIntはこれまで説明してきた普通の数値(number型)とは別の型であり、任意精度の整数を表す型です。ざっくり言えば、ビット数という制限に囚われずに好きなだけ大きい整数を表すことができるという、従来の数値とはまったく異なる特徴を持つ値です。ただし、小数は範疇外です。あくまで整数のみが対象となります。

BigInt型の値を作るには主に2つの方法があります。一つはBigIntリテラルを使う方法です。これは123nのように整数の後にnを付けるリテラルを用います。

もう一つはBigInt()関数を使う方法です。これはNumber()のように、与えられた値をBigInt型の値に変換してくれます。整数を表す数値や文字列をBigInt()に与えることでBigInt型の値を作ることができるのです。

BigInt型の値はtypeof演算子で調べると"bigint"という結果になります。

console.log(123n);                   // 123n

console.log(typeof 123n); // "bigint"
console.log(BigInt(123) === 123n); // true
console.log(BigInt("123") === 123n); // true

BigInt型の値は、普通の数値と同様に四則演算が可能です。ただし、これはBigInt同士の計算に制限されています。BigIntと普通の数値を混ぜるとエラーになります。また、BigIntは小数を表しませんので、割り算が割り切れない場合は切り捨てられます。

console.log(2n + 3n); // 5n

console.log(2n * 3n); // 6n
console.log(5n / 2n); // 2n

BigIntが普通の数値とは異なり精度に制限がないことを確かめてみましょう。

console.log(2 ** 60 + 10 === 2 ** 60);      // true (普通の数値は精度が53ビットしかないので)

console.log(2n ** 60n + 10n === 2n ** 60n); // false (BigIntはどんな大きさの整数も正確に表現可能)

このような挙動はたいへんうれしいですね。とりわけ、これまでJavaScriptで扱うのが難しかった64ビット整数がBigIntを使えば自然に表せるのがとても偉いです。BigIntの導入以降は、53ビット以上に大きな整数が必要な場面でBigIntが活用されていくことになります(cf. JavaScriptの日時処理はこう変わる! Temporal入門)。

また、BigIntは任意精度のビット演算ができるのも嬉しい点です。従来の数値型では32ビットに制限されていたビット演算が、BigIntでは任意の精度で可能です。

console.log((2n ** 60n | 2n ** 55n === 2n ** 60n + 2n ** 55n); // true


BigInt関連のメソッド

以上がBigIntの基本機能です。また、以下の2つのメソッドがBigIntに関連して提供される予定です。


BigInt.asUintN, BigInt.asIntN

BigIntの値を指定したビット数に制限して得られる新しいBigInt値を返します。例えばBigInt.asUintN(64, n)nの下位64ビットの値を表すBigIntです。asUintNasIntNの違いは、得られた下位nビット分の値を符号なし整数として解釈するか符号あり整数として解釈するかの違いです。

console.log(BigInt.asUintN(64, 7n * (2n ** 62n)) === 2n ** 63n + 2n ** 62n); // true


BigIntの上限

ちなみに、BigIntはどれくらい大きな整数を表せるのでしょうか。実は、仕様ではBigIntの上限は定められていません。理想的な処理系では、どんなに大きなBigIntでも表せることになります。

ところが、現実にはそうもいきません。最悪のケースでも、コンピュータのメモリを全部食いつぶしてしまえばそれ以上の大きさのBigIntは作れないわけです。

もちろん、現実の処理系はそういう事態になるより前に何らかのエラーを発生させるでしょう。

これについては、BigIntの上限を調べてみた方がいるようです。それによれば、Google ChromeではBigIntの上限$M$は$2^{2^{30}-1} \leq M \leq 2^{2^{30}}$を満たすようです。恐らくビット数で制限されているであろうことを考えると、ChromeではBigIntの最大精度は$2^{30}$ビットと考えてよさそうです。$2^{30}$ビットというのは128MiBですから、ChromeはひとつのBigIntに対して128MiBまでの使用を許してくれるようです(複数のBigIntを同時に作る実験は行われていないので、複数のBigIntの合計が128MiBとかそういう話かもしれません。未検証です)。

もっとも、BigIntの演算というのは定数時間ではないので、実際の128MiBのメモリを使って表されたBigIntに対して計算を行うのは非常に時間がかかることでしょう(前述の記事でも実際にそのような結果が報告されています)。


まとめ

この記事ではJavaScriptの数値型について概観しました。

ポイントは、(BigIntではない従来の)数値型は整数と小数を区別せず、IEEE 754倍精度浮動小数点数で表されているという点です。その結果、整数が53ビットという一見中途半端な精度を持っていたり、浮動小数点数に特有の(0.1 + 0.2 !== 0.3のような)挙動が表れます。特に、どの挙動がJavaScriptに特有の話でどの挙動がIEEE 754由来の話なのかはよく考えておくべきでしょう。万一にもIEEE 754をバカにするつもりでJavaScriptをバカにしてしまったら末代までの恥です。

とはいえ、JavaScriptも歴史ある言語ですから、数値周りの挙動も中々面白いものが出来上がっています。そのあたりをこの記事で楽しんでいただけたなら嬉しいです。





  1. これは静的型にも動的型にも言えることですね。 



  2. usizeとかisizeがちょっと厄介なのですが、ここではあまり関係がないので触れるのを避けることにします。 



  3. 本当の処理系の内部では最適化して整数として扱われている可能性もありますが、仕様上は全て浮動小数点数として扱われています。 



  4. 筆算してみたけど結果が全然違うじゃないかと思われる方がいるかもしれませんが、0.10.2では指数部(後述)が異なるのでそれによる丸め誤差が発生しているからです。なので、本当はこうやって割り切れるまで10進展開した値を書くことにはそこまで意味がないのですが、ここでは何となく誤差があるんだよということを認識してもらうのが目的なので大目に見てください。 



  5. 複雑な計算の場合は途中の計算もsafe integerになっていないといけませんが。 



  6. 正確なというのは、53ビットで表せない範囲の部分をちゃんと最後まで10進展開してくれるという意味です(詳しくはtoStringのところで説明します)。 



  7. ただし、前述のnumeric separators(1_234_567みたいなやつ)が導入された場合はNumberはこれを解釈してくれないという仕様になる予定のようです。