数値というのはプログラミングにおいて極めて基本的な対象です。ほとんどのプログラミング言語は何らかの形で数値の操作を行うことができ、もちろん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.0
や2.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.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ビットぶんの値である$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進数で表すと100000000000000000000000000000000000000000000000000000
(1
の後に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に特有の話ではないということは理解しておくべきでしょう。
NaN
とInfinity
, +0
と-0
JavaScriptにはNaN
とInfinity
という特別な数値が存在します。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
とすればいいので特に意味はありませんが、整数と浮動小数点数を区別する言語では123
と123.0
が別の意味になるために後者を省略したい場合の記法として需要があるようです。
なお、数値のメソッドを呼びたい場合には注意してください。123
のtoFixed
メソッド(後述)を呼びたい場合には、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-4
(0.0001
と同じ)のようにe
の後ろは負の整数も可能です。
また、ここまでは10進数表記でしたが、他に16進数・8進数・2進数のリテラルがサポートされています。それぞれ0xabcdef
、0o755
、0b1010111
のようなリテラルです。これらの10進数以外のリテラルは小数点やe
による指数表記はサポートされていません。
また、桁数の多い数値リテラルを見やすく1_234_567
のように書ける提案がもう少しで完成しそうです(cf. JavaScriptで数値の区切り文字を使いたい物語)。
以上が数値リテラルの話でした。まあ、特におかしな所はありませんでしたね。では、いよいよ数値演算の話に入っていきます。
JavaScriptにおける数値演算
とはいえ、JavaScriptの数値演算は、それほど特筆すべき点があるわけではありません。まず、普通の四則演算(+
, -
, *
, /
及び余り%
)と累乗(**
)が備えられています。
ただし、+
は文字列の連結にも使われるので"5" + 1
が"51"
になったりする点はやや注意が必要でしょうか。-
とかは両辺を数値に変換するので"5" - 1
は4
です。これらの演算子にオブジェクトを渡してしまったときの挙動はちょっと面白かったりするのですが(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}$)に対する結果がtoString
とtoFixed
で違っているという例でしたね。
console.log((2 ** 60).toString()); // "1152921504606847000"
console.log((2 ** 60).toFixed()); // "1152921504606846976"
$2^{60}$は正確に1152921504606846976
ですが、(2 ** 60).toString()
は最後の4桁が7000
と大ざっぱになっています。
しかし、JavaScriptの整数の精度が53ビットしかないことを考えれば、実は1152921504606847000
も2 ** 60
になることが分かります。
console.log(2 ** 60 === 1152921504606846976); // true
console.log(2 ** 60 === 1152921504606847000); // true
$2^{60}$という61ビットの数が53ビットに情報を減らされる場合、8ビット分情報が落ちます。こうなると、丸めのことを考えてもその半分(7ビット)程度の違いは意味を成さなくなります。$2^7$は128ですから、6976
と7000
の間のたった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
加えて、parseFloat
はInfinity
に対する特別なサポートがあります。
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
その他の数値関係メソッド
他にもいくつか数値関係のメソッドがあるので紹介します。
isNaN
・isFinite
isNaN
は引数で与えられた数がNaN
かどうか判定するメソッドです。
console.log(isNaN(123)); // false
console.log(isNaN(Infinity)); // false
console.log(isNaN(NaN)); // true
NaN
はNaN === NaN
がfalse
になってしまうため、isNaN
がNaN
かどうか判定する簡単な方法です。
また、isFinite
は与えられた数がNaN
かInfinity
または-Infifity
だったらfalse
で他はtrue
を返すメソッドです。これは意外と使いどころがある関数です。
console.log(isFinite(123.45)); // true
console.log(isFinite(NaN)); // false
console.log(isFinite(Infinity)); // false
parseInt
などと同様にこれらにもNumber
の下にあるバージョン、すなわちNumber.isNaN
とNumber.isFinite
がありますが、何とこれらはisNaN
やisFinite
とは微妙に挙動が違います。
Number
バージョン、すなわちNumber.isNaN
やNumber.isFinite
は、与えられたものが数値でない場合は即座にfalse
を返します。一方、グローバルのisNaN
やisFinite
はまず与えられたものを(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 / 2
は0
となります。
他にはNumber.POSITIVE_INFINITY
(Infinity
が入っている)とNumber.NEGATIVE_INFINITY
(-Infinity
が入っている)、そしてNumber.NaN
があります(NaN
が入っている)があります。グローバル変数のNaN
やInfinity
が信用ならないときに使いましょう。
最後に、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
です。asUintN
とasIntN
の違いは、得られた下位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も歴史ある言語ですから、数値周りの挙動も中々面白いものが出来上がっています。そのあたりをこの記事で楽しんでいただけたなら嬉しいです。
-
これは静的型にも動的型にも言えることですね。 ↩
-
usize
とかisize
がちょっと厄介なのですが、ここではあまり関係がないので触れるのを避けることにします。 ↩ -
本当の処理系の内部では最適化して整数として扱われている可能性もありますが、仕様上は全て浮動小数点数として扱われています。 ↩
-
筆算してみたけど結果が全然違うじゃないかと思われる方がいるかもしれませんが、
0.1
と0.2
では指数部(後述)が異なるのでそれによる丸め誤差が発生しているからです。なので、本当はこうやって割り切れるまで10進展開した値を書くことにはそこまで意味がないのですが、ここでは何となく誤差があるんだよということを認識してもらうのが目的なので大目に見てください。 ↩ -
複雑な計算の場合は途中の計算もsafe integerになっていないといけませんが。 ↩
-
正確なというのは、53ビットで表せない範囲の部分をちゃんと最後まで10進展開してくれるという意味です(詳しくは
toString
のところで説明します)。 ↩ -
ただし、前述のnumeric separators(
1_234_567
みたいなやつ)が導入された場合はNumber
はこれを解釈してくれないという仕様になる予定のようです。 ↩