NaNはNot a NumberだけどNumber型である話
この記事では、JavaScriptのNaN
について改めて学ぶという趣旨の話をします。
JavaScriptで、文字列などから数値へ値を変換したいことがあると思います。 典型的なケースでは、ユーザーに入力してもらった数字となる文字列を、Number型へ変換するというケースです。
この場合、Number
コンストラクタ関数やNumber.parseInt
、Number.parseFloat
などが利用できます。(ここでは、Number.parseInt
にしていますが、parseInt
と同じです)
// ユーザー入力を文字列として受け取る
var input = window.prompt("数字を入力してください", "42");
// 文字列を数値に変換する
var number = Number(input);
console.log(typeof number); // => "number";
console.log(number); // 入力された文字列を数値に変換したもの
しかし、Number
コンストラクタ関数やNumber.parseInt
を使って、値を明示的に変換したとしても結果はNaN
になる場合があります。
端的に言えば、Number型と互換性のない性質のデータをNumber型へ変換した結果はNaN
となります。
// 数字ではないため、数値へは変換できない
Number("文字列"); // => NaN
// 整数としてパースできない文字列だった
Number.parseInt("e10s", 10); // => NaN
// 未定義の値はNaNになる
Number(undefined); // => NaN
// 空オブジェクトは数値にできない
Number({}); // => NaN
JavaScriptでは数値にIEEE 754を採用していて、NaN
もIEEE 754で定義されている値です。
NaN
はNot a Numberの略称で、特殊な性質をもつNumber型のデータです。
また、NaN
は何と演算しても結果はNaN
になる特殊な値です。
次のように、計算の途中で値がNaN
になると、最終的な結果もNaN
となります。
var x = 10;
var y = x + NaN;
var z = y + 20;
console.log(x); // => 10
console.log(y); // => NaN
console.log(z); // => NaN
しかし、NaN
はNumber型の一種であるという名前と矛盾したデータに見えますが仕様です。
// NaNはnumber型
typeof NaN; // => "number"
NaNしか持っていない特殊な性質として、自分自身と一致しないという特徴があります。
この特徴を利用することで、ある値がNaN
であるかを判定することもできます。
function isNaN(x) {
// NaNは自分自身と一致しない
return x !== x;
}
isNaN(1); // => false
isNaN("str"); // => false
isNaN({}); // => false
isNaN([]); // => false
isNaN(NaN); // => true
同様の処理を行う方法としてES2015からNumber.isNaN(x)
メソッドが追加されています。
実際に値がNaN
かを判定する際には、Number.isNaN(x)
メソッドを利用するとよいでしょう。
Number.isNaN(NaN); // => true
注記: isNaN()というグローバル関数もありますが、こちらは引数を暗黙的にNumber型へ変換してからNaN
かどうかを判定します。
暗黙的な変換をしてしまうことで、明らかにバグっているような挙動を引き起こすため使わないようにしましょう。
// グローバルの`isNaN`関数
// 上で作ったisNaN関数とは別もの
isNaN("string"); // => true
// `isNaN`関数は以下のようなことをやっているのと同じ
// Numberコンストラクタ関数で変換してから、`Number.isNumber`で判定するのと同じようなもの
Number.isNumber(Number("string"));// => true
// 安全に使うならxが number か判定してから isNaNで判定する
typeof x === "number" && isNaN(x);
NaN
は暗黙的な型変換の中でももっとも避けたい値となります。
理由として、先ほど紹介したようにNaN
は何と演算しても結果がNaN
となってしまうためです。
これにより、計算していた値がどこでNaN
となったのかが分かりにくく、デバッグが難しくなります。
たとえば、次のsum
関数は可変長引数(任意の個数の引数)を受け取り、その合計値を返します。
しかし、sum(x, y, z)
と呼び出した時の結果がNaN
になってしまいました。
// 任意の個数の数値を受け取り、その合計値を返す関数
function sum(...values) {
return values.reduce((total, value) => {
return total + value;
}, 0);
}
var x = 1, y, z = 10;
sum(x, y, z); // => NaN
よく注意して見ると、y
の値が未定義となっています。
そのため、sum(1, undefined, 10);
と呼ばれていたのと同じ結果になります。
sum(1, undefined, 10); // => NaN
// 計算中にNaNとなったため最終結果もNaNになった
1 + undefined; // => NaN
NaN + 10; // => NaN
これは、sum関数
において引数を明示的にNumber型へ変換したとしても回避することはできません。
つまり、次のように明示的な型変換しても解決できない問題あることが分かります。
// `value`をNumberで明示的に変換して扱ったバージョン
function sum(...values) {
return values.reduce((total, value) => {
return total + Number(value);
}, 0);
}
var x = 1, y, z = 10;
sum(x, y, z); // => NaN
この意図しないNaN
への変換を避ける方法として、大きく分けて2つの方法があります。
sum
関数側(呼ばれる側)で、Number型の値以外を受け付けなくするsum
関数を呼び出す側で、Number型の値のみを渡すようにする
つまり、呼び出す側または呼び出される側で対処するということですが、 どちらも行うことがより安全なコードにつながります。
そのためには、sum
関数が数値のみを受け付けるということを明示する必要があります。
明示する方法としてsum
関数のドキュメント(コメント)として記述したり、
引数に数値以外の値がある場合は例外を投げるという処理を追加するといった形です。
JavaScriptではコメントで引数の型を記述する書式としてJSDocが有名です。 また、TypeScriptやFlowを使うことで型定義や静的チェックもできます。
ランタイム時に値をチェックし例外または警告を表示する関数としてconsole.assert(条件式, メッセージ)
メソッドが多くの実行環境で利用できます。
(Node.jsだと例外をなげ、ブラウザだとコンソールへの警告になります)
この2つを利用してsum
関数の前提条件を詳細に実装したものは次のようになります。
/**
* 数値を合計した値を返します。
* 一つ以上の数値と共に呼び出す必要があります。
* @param {...number} values
* @returns {number}
**/
function sum(...values) {
return values.reduce((total, value) => {
// 第一引数の評価結果がtrueではない場合、第二引数のメッセージが警告として出る
console.assert(typeof value === "number", `${value}はNumber型ではありません`);
return total + Number(value);
}, 0);
}
var x = 1, y, z = 10;
console.log(x, y, z);
sum(x, y, z); // => AssertionError
JSDocに書いたことをconsole.assert
でも実装しているのが気になります。
Babelを使って変換している場合はbabel-preset-jsdoc-to-assertを使うことで、JSDocから同等のassertionへ自動的に変換することができます。
おまけ: さらに意地悪なことをすると、先ほどのsum
関数に引数としてNaN
を渡すと、NaN
はNumber型なのでassertをすり抜けることができます。
そのため、typeofではなく、Number.isFinite(value)
を使い有限数かをチェックし、コメントもそのように書き換えるとより渡す値が明確になります。
/**
* 数値を合計した値を返します。
* 一つ以上の有限数と共に呼び出す必要があります。
* @param {...number} values
* @returns {number}
**/
function sum(...values) {
return values.reduce((total, value) => {
// NaNがきたらassertで落ちる
console.assert(Number.isFinite(value), `${value}は有限数ではありません`);
return total + Number(value);
}, 0);
}
var x = 1, y = NaN, z = 10;
console.log(x, y, z);
sum(x, y, z); // => AssertionError
console.assert
なども今はunassertを使えばproductionビルドからは簡単に取り除けるので、気軽に使うようにしても問題ないと思います。
このように、sum
関数はどのように使うべきかを明示することで、
エラーとなった時に呼ばれる側と呼び出し側でどちらに問題があるのかが明確になります。
この場合は、sum
関数へundefined
な値を渡している呼び出し側に問題があります。
関数/モジュールの責務をどう実装するかという設計の方針としては、防御的プログラミングや契約による設計など色々あると思いますが、誰かが分かりやすく解説してくれると信じています。
おわりに
JavaScriptは、Number型へ値を変換した場合にNaN
となってしまうことがあります。
多くの場合、何らかの暗黙的な型変換が処理中に発生してNaN
となっていることが多いです。
(またはparseInt
後にNumber.isNaN
でチェックしていないなど)
そのため、JavaScriptでアプリケーションを書く場合は、このような検出しにくいバグを見つけられるように書くことは重要です。
というような話を暗黙的な型変換 · JavaScriptの入門書 #jsprimerで書きました。(暗黙的な型変換についてがメインでNaN
はおまけ)
まだまだ書いてる最中なので、興味がある人はasciidwango/js-primer: JavaScriptの入門書を見てみてください。
基本の基本は書いたのですが、まだどういうことを書くべきか迷っている部分も多いので、以下のIssueに意見をください。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。