先日、このようなツイートを書きました。
久しぶりの JavaScript クイズ。
— Takuo Kihira (@tkihira) September 7, 2021
JavaScript において NaN === NaN の結果は次のうちどれになるでしょうか?
答えは 4 の「状況によって上記以外もありうる」です。でも、2 や 3 を選んだ方も、もはや正解だといって差し支えないと思います。
解説が長くなったので、ブログ記事にまとめました。
そもそも NaN とは
NaN
は “Not a Number” を意味する数値です。数値なのに「Not a Number」というのは違和感があるかもしれませんが、数値表現することが出来ない状態を保持するために便宜的に用意された数値、というようなものです。
NaN
は、浮動小数点演算において数値では表現出来ない計算をしようとすると登場します。例えば JavaScript において 0 / 0
の計算結果は NaN
になります。Infinity * 0
も NaN
です。実際の 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 に限らず、NaN
と NaN
の比較は 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]×6
と 0111 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 === NaN
が false
になる、ということをご理解いただけたかと思います。では冒頭の問題は 2 番が正解なのでしょうか?
しかし、状況によっては NaN === NaN
が true
になることがあります。ここではそれをご説明しましょう。
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 }.
実は undefined
や NaN
は、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 === NaN
が true
になる状況もあり得るということになります。
{
const NaN = 3;
console.log(NaN === NaN); // → true
}
NaN === NaN は常に boolean なのか
以上より、NaN === NaN
が true
にもなりえる、ということをご理解いただけたかと思います。では冒頭の問題は 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
の伝搬しないコードを書くように心がけましょう。