nmi.jp Twitter → @tkihira
10 年前に JavaScript で Flash Player を開発し買収された話

JavaScript クイズ解説: NaN === NaN の結果はどうなる?


2021-09-09
Takuo Kihira

先日、このようなツイートを書きました。

答えは 4 の「状況によって上記以外もありうる」です。でも、2 や 3 を選んだ方も、もはや正解だといって差し支えないと思います。

解説が長くなったので、ブログ記事にまとめました。

そもそも NaN とは

NaN は “Not a Number” を意味する数値です。数値なのに「Not a Number」というのは違和感があるかもしれませんが、数値表現することが出来ない状態を保持するために便宜的に用意された数値、というようなものです。

NaN は、浮動小数点演算において数値では表現出来ない計算をしようとすると登場します。例えば JavaScript において 0 / 0 の計算結果は NaN になります。Infinity * 0NaN です。実際の JavaScript のプログラムでは、"string" - 0 のように、他の型を数値に変換した時に発生することが多いでしょう。

JavaScript における NaN に対する数値演算は、常に NaN になります。一度 NaN が登場すると、それが伝搬してしまい、思わぬところで NaN が登場してしまうことがあります。

NaN は falsy であり、boolean に変換すると false になります。しかし、他の falsy な値と違い、false と比較しても false を返します。

console.log(NaN == false); // → false
console.log("" == false); // → true
console.log(0 == false); // → true

これは数値型と boolean の比較の場合は boolean が数値に変換されて比較される仕様の影響であり、この場合は NaN === 0 という比較になり、結果 false が返っています。少し特殊な状況ですね。

NaN の比較

さて、今回のツイートの問題である NaN 同士の比較についてですが、これはなんと false になります。

console.log(NaN === NaN); // → false

JavaScript(ECMAScript) では NaN の扱い方は明確に仕様で決められています。

https://tc39.es/ecma262/#sec-numeric-types-number-equal

Number::equal ( x, y )

The abstract operation Number::equal takes arguments x (a Number) and y (a Number). It performs the following steps when called:

1. If x is NaN, return false.
2. If y is NaN, return false.
3. If x is the same Number value as y, return true.
4. If x is +0𝔽 and y is -0𝔽, return true.
5. If x is -0𝔽 and y is +0𝔽, return true.
6. Return false.

このように、どちらかが NaN であれば、常に false が返るようになっています。

JavaScript における NaN に対する比較は not equal 以外はすべて false が返ります。not equal は true が返ります。

console.log(NaN === NaN); // → false
console.log(NaN > NaN); // → false
console.log(NaN >= NaN); // → false
console.log(NaN < NaN); // → false
console.log(NaN <= NaN); // → false
console.log(NaN !== NaN); // → true

このような仕様に決まったのは、浮動小数点演算の標準規格である IEEE 754 の影響です。IEEE 754 内で、NaN に対する比較が上記のように定義されています。

JavaScript 以外の言語でも、大抵は IEEE 754 に準拠しており、同じ比較結果になることが期待されます。JavaScript に限らず、NaNNaN の比較は false になることが多い、という風に理解しておくと良いと思います。

余談1: NaN のビット表現

NaN は IEEE 754 において、複数通りのビット表現を持つことが許されています。NaN 以外のすべての浮動小数点の数値は +Infinity -Infinity +0 -0 を含め、すべて一意のビット表現を持ちます。しかし NaN だけは、指数部のビットがすべて 1 であれば、仮数部に 0 以外の何を持っても NaN になると定義されています(仮数部が 0 になると Infinity になります)。

これを、TypedArray を用いて実際に確かめてみることが出来ます(このコードはリトルエンディアン前提です)。

var floatView = new Float64Array(1);
var charView = new Uint8Array(floatView.buffer);
floatView[0] = NaN;
console.log(charView.join(',')); // → 0,0,0,0,0,0,248,127
// 127 -> 0111 1111
// 248 -> 1111 1000
charView[6] = 0xf0; // 1111 0000
console.log(floatView[0]); // → Infinity: 仮数部が全部 0 のため
charView[6] = 0xf4 // 1111 0100
console.log(floatView[0]); // → NaN: 仮数部にビットが立っているため

この例でいうと、最初の 0111 1111 1111 1000 [0000 0000]×60111 1111 1111 0100 [0000 0000]×6 ではビット表現が違いますが、両者とも同じように NaN を表現していることがわかります。

ここまでのまとめ

JavaScript では NaN 同士の比較は false になります

なので例えば、

if (value === NaN) {
  // value が NaN だった時の処理
  ...
}

みたいなコードは、意図通りに動かないので気をつけましょう。こういう場合は次のように書きます

if (Number.isNaN(value)) {
  // value が NaN だった時の処理
  ...
}

余談2: JavaScript の isNaN と Number.isNaN

グローバルオブジェクトにある isNaN 関数と、ビルトインオブジェクト Number のプロパティにある isNaN 関数は挙動が違うので気をつけましょう。

入力 isNaN Number.isNaN
NaN true true
0 false false
“string” true false

isNaN は、入力を数値に変換すると NaN になる場合に true を返します。一方で Number.isNaN は入力が NaN そのものであった場合のみ true を返します。

余談3: JavaScript ビルトイン関数の NaN 比較問題

配列の中にある NaN を探す時に、この NaN の仕様のせいで少しおかしなことになりますので注意しましょう。

var a = [3, 1, 4, 1, 5, NaN, 2];
console.log(a.indexOf(NaN)); // → -1  !!!!!
console.log(a.includes(NaN)); // → true
console.log(a.findIndex(v => Number.isNaN(v))); // → 5

このように、indexOf で NaN を探そうとしても見つけることが出来ません。内部で NaN === NaN を呼んで、それが false を返してしまっているからです。

NaN === NaN は常に false なのか

以上より、NaN === NaNfalse になる、ということをご理解いただけたかと思います。では冒頭の問題は 2 番が正解なのでしょうか?

しかし、状況によっては NaN === NaNtrue になることがあります。ここではそれをご説明しましょう。

NaN は JavaScript(ECMAScript) において次のように定義されています。

https://tc39.es/ecma262/#sec-value-properties-of-the-global-object-nan

19.1 Value Properties of the Global Object

(略)

19.1.3 NaN
The value of NaN is NaN (see 6.1.6.1). This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }.

19.1.4 undefined
The value of undefined is undefined (see 6.1.1). This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }.

実は undefinedNaN は、null などのキーワードとは違い、グローバルオブジェクトのプロパティとして定義されているのです。幸い [[Writable]]: false なので上書きすることは出来ませんが、上書きしようとすること自体は可能です。

undefined = 7;
NaN = "hello world";
console.log(undefined); // → undefined
console.log(NaN); // → NaN
// null = 0; // Syntax Error!

グローバルスコープだと上書きは出来ないのですが、ローカルにスコープを作成すれば、その中で値を自由に書き換えることは可能です。

{
  const undefined = 7;
  let NaN = "hello world";
  console.log(undefined); // → 7
  console.log(NaN); // → hello world
}

もちろんこんなコードは絶対に書かないで下さい。しかし理論上 NaN の値を書き換えることが可能なため、 NaN === NaNtrue になる状況もあり得るということになります。

{
  const NaN = 3;
  console.log(NaN === NaN); // → true
}

NaN === NaN は常に boolean なのか

以上より、NaN === NaNtrue にもなりえる、ということをご理解いただけたかと思います。では冒頭の問題は 3 番が正解なのでしょうか?

限りなく 3 番で正解だと言ってよいのですが、しかしわずかに例外を作成することが出来ます。JavaScript(ECMAScript) の === の挙動の仕様を見てみましょう。

https://tc39.es/ecma262/#sec-equality-operators-runtime-semantics-evaluation

EqualityExpression : EqualityExpression === RelationalExpression
1. Let lref be the result of evaluating EqualityExpression.
2. Let lval be ? GetValue(lref).
3. Let rref be the result of evaluating RelationalExpression.
4. Let rval be ? GetValue(rref).
5. Return IsStrictlyEqual(rval, lval).

IsStrictlyEqual は必ず boolean が返ってくるので、この処理は常に boolean が返ってきそうに思えますが、罠が一つあります。GetValue は例外を出すことが出来るのです。なので、NaN を評価するときに例外を投げることが出来れば、true/false 以外の結果となります。

しかし上記の例だと、NaN は変数へのアクセスになります。変数へのアクセスで例外を出すことは出来ません。globalThis.NaN の読み込み時に getter を設定することが出来ればよいのですが、NaN[[Writable]]: false でもあるため、次のコードは失敗します。

Object.defineProperty(globalThis, 'NaN', {get: () => { throw "Yay!"; }});
// TypeError: Cannot redefine property: NaN

ではどうするか…。

ここで、禁断の with 構文を使うのです。

var o = {};
Object.defineProperty(o, 'NaN', {get: () => { throw "Yay!"; }});
with (o) {
    console.log(NaN === NaN); // Uncaught: "Yay!"
}

with 構文は、その複雑怪奇な仕様、かつ直感に反する挙動により、数多のバグを世の中に送り出した忌むべき構文となっており、使ってはいけない構文の筆頭です。strict mode では、そもそも利用することすら出来ません。みなさんも存在すら忘れていたのではないでしょうか。そのまま忘れていて良いと思います

なんにせよ、NaN === NaN で見事例外を投げることが出来ました!よって、冒頭の問題の答えは 4 の「状況によっては上記以外もありうる」が正解になります。

まとめ

長々と書いてきましたが、 NaN === NaN は false である、ということだけ知っておけば問題ありません。それ以降の話は、細かい仕様の隅をついた、JavaScript ならではの汚いテクニックの話でした。冒頭の問題で 2 3 4 を選んだ方は十分です。1 を選んだ方にとって、この記事が NaN の特性を学ぶきっかけになれば幸いです。

undefined と同じく、NaN もコード中に出てきたらバグの発生源になりかねない危険な数値です。どのような場合に NaN が発生するのかを理解し、NaN の伝搬しないコードを書くように心がけましょう。