NaN === NaN が false になる理由:JavaScript の ===・Object.is・includes の比較の違い

JavaScript では NaN === NaN が false なのに [NaN].includes(NaN) は true です。===、Object.is、includes・Map・Set が NaN と -0 をどう扱うかを ECMA-262 の規則とコードで整理しました。

開発JavaScript数値

NaN === NaN は false です。ところが [NaN].includes(NaN) は true で、[NaN].indexOf(NaN) は -1 です。バグのように見えますが、3つとも仕様どおりの結果です。JavaScript の標準である ECMA-262 には、2つの値が「同じ」かを判定するアルゴリズムが複数あり、演算子やメソッドごとに使うものが違います。

以下の仕様の内容は、2026年10月3日付の ECMA-262 最新ドラフトに基づいています。コードの結果は Node.js 24.13.0 で実行して確かめました。

結果が分かれるのは2つの場合だけ

仕様の比較アルゴリズムは3つあります。厳密等価の比較(IsStrictlyEqual)、SameValue、SameValueZero です。どれもまず2つの値の型を確認し、型が違えばすぐに false を返します。数値以外の値では、3つとも同じ手順に進みます。文字列は同じコードユニットが同じ順に並んでいるか、オブジェクトは同一のオブジェクトかを見ます。

3つのアルゴリズムの結果が分かれるのは、NaN と NaN を比べる場合と、+0 と -0 を比べる場合の2つだけです。NaN と 1 のような、それ以外の数値の組み合わせでは3つとも同じ答えになります。

アルゴリズム NaN と NaN +0 と -0 使われる場所
IsStrictlyEqual 異なる 等しい ===、indexOf、lastIndexOf、switch の case
SameValue 等しい 異なる Object.is
SameValueZero 等しい 等しい includes、Map のキー、Set の値

== は2つの値の型が同じなら === と同じ結果になります。そのため NaN == NaN も false です。

NaN 同士を比べても等しくならないのはなぜか

=== が数値の比較に使う手順(Number::equal)は、最初の2つが「x が NaN なら false を返す」「y が NaN なら false を返す」です。比較相手が何であっても、同じ NaN 同士で比べても、どちらか一方が NaN であれば結果は false です。

JavaScript の Number は IEEE 754 の倍精度(binary64)の値です。IEEE 754 には NaN を表すビットパターンが多数ありますが、ECMA-262 はそれらを単一の NaN 値として扱い、JavaScript のコードからは区別できないと定めています。NaN が1つしかないので、「NaN 同士は等しい」とみなす SameValue と SameValueZero も成り立ちます。

NaN === NaN;               // false
Object.is(NaN, NaN);       // true
[NaN].indexOf(NaN);        // -1
[NaN].includes(NaN);       // true
new Set([NaN, NaN]).size;  // 1

配列の中の NaN の位置が必要なら、indexOf ではなく findIndex(Number.isNaN) を使います。NaN かどうかを調べる関数にも注意が必要です。グローバルの isNaN は引数を先に数値に変換するため、isNaN("abc") は true です。Number.isNaN は変換しないので、同じ入力に対して false を返します。仕様には、x !== x が true になるのは x が NaN のときだけだという注記もあります。

-0 はどこで生まれ、なぜ見落としやすいのか

Number には +0 と -0 が別々にあります。=== は2つを等しいとみなし、Object.is は異なるとみなします。

-0 を見落としやすい理由の1つは文字列への変換です。数値を文字列にする規則(Number::toString)は +0 にも -0 にも "0" を返します。そのため String(r) や、文字列を連結して作ったログメッセージでは -0 も 0 と表示されます。開発者ツールのコンソールのように値そのものを表示する場所では、-0 と表示されることがあります。

const r = Math.round(-0.4);
String(r);          // "0"
r === 0;            // true
Object.is(r, -0);   // true
1 / r;              // -Infinity

Math.round は、-0.5 以上かつ 0 未満の値に対して -0 を返すと仕様で定められています。正の数を -0 で割ると -Infinity になるので、こうした値が割り算に入ると符号の違いが結果に表れます。+0 と -0 を区別する必要があるときは Object.is を使います。

includes・Map・Set は 0 を1つとして扱う

Array.prototype.includes は SameValueZero で比較します。仕様は indexOf との違いを2つ明記しています。NaN を見つけられること、そして配列の空きスロットを飛ばさずに undefined として扱うことです。

[-0].includes(0);            // true
[, 1].includes(undefined);   // true
[, 1].indexOf(undefined);    // -1

仕様は、Map のキーと Set の値も SameValueZero の意味で区別すると、それぞれの章の冒頭で定めています。実際のアルゴリズムは、値が -0 ならまず +0 に置き換え、そのあと SameValue で比較する形で書かれています。そのため -0 で入れたキーを取り出すと +0 になります。

const m = new Map([[-0, "zero"]]);
m.get(0);                          // "zero"
Object.is([...m.keys()][0], -0);   // false

どの比較を使うか

  • 通常の比較は ===
  • 値が NaN かどうかの確認は Number.isNaN(x)
  • 配列に NaN があるかは includes、位置も必要なら findIndex(Number.isNaN)
  • +0 と -0 まで区別するなら Object.is

オブジェクトはどの方法でも同一のオブジェクトかどうかだけを見ます。中身が同じでも Object.is({}, {}) は false です。

ブログへ