この記事では、JavaScriptのNaNについて改めて学ぶという趣旨の話をします。

JavaScriptで、文字列などから数値へ値を変換したいことがあると思います。 典型的なケースでは、ユーザーに入力してもらった数字となる文字列を、Number型へ変換するというケースです。

この場合、Numberコンストラクタ関数やNumber.parseIntNumber.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が有名です。 また、TypeScriptFlowを使うことで型定義や静的チェックもできます。

ランタイム時に値をチェックし例外または警告を表示する関数として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に意見をください。