Nullの奇妙な比較、あるいはJavaScriptの仕様を読むことが重要な理由

以下はJavascript : The Curious Case of Null >= 0の戸田奈津子訳です。

Why it's important to read the Javascript Spec

1-F-EDLK-OugJ_4KOgtJqnPA.png

JavaScriptの速成コースで作ったソースをコンパイルしていたところ、nullの挙動について面白い現象を発見したので周囲に尋ねてみた。

    null > 0;  // false
    null == 0; // false

    null >= 0; // true

ちょっと待って、これは何?
全くもってナンセンスだ。

いったいどのような値が、0より大きくなく、0と等しくもなく、0と等しいかそれより大きい状態になるというのだ?

当初、私はこれはJavaScriptだから仕方ないとスルーしていたが、しかしこの動作はとても奇妙で、ついつい興味をそそられてしまった。
null型とその扱いにどのような関係があるのか、また関係性と等価性のチェックはどのように行われるのだろうか?

そこで、私はこの現象の根本的原因を突き止めるため、JavaScriptの信頼できる唯一の情報源を探すことにした。
つまり、JavaScriptの仕様だ。

そして私は幻覚的な体験を経験したのだった。

The Abstract Relational Comparison Algorithm

まずは比較アルゴリズムを見てみよう。

    null > 0; // false

仕様によると、演算子><は、Abstract Relational Comparison Algorithmというアルゴリズムに従って文がtruefalseかを返す。
以下はx < yの比較アルゴリズムである。

1. ToPrimitive(x, Number)を呼んでxをプリミティブ型にする。
2. ToPrimitive(y, Number)を呼んでyをプリミティブ型にする。
3. xの型とyの型が共にStringであれば、16に進む。
4. ToNumber(x)を呼んでxをNumber型にする。
5. ToNumber(y)を呼んでyをNumber型にする。
6. xがNaNであれば`undefined`を返して終了。
7. yがNaNであれば`undefined`を返して終了。
8. xとyの値が同じであればfalseを返して終了。
9. xが+0でyが-0であればfalseを返して終了。
10. xが-0でyが+0であればfalseを返して終了。
11. xが+∞であればfalseを返して終了。
12. yが+∞であればtrueを返して終了。
13. yが-∞であればfalseを返して終了。
14. xが-∞であればtrueを返して終了。
15. xの数値がyの数値より小さければtrueを返して終了。それ以外ならfalseを返して終了。この時点でxy両方とも無限大でも0でもない値であることが保証されている。
16. yの値がxの値のprefixである場合、falseを返して終了。(文字列qが文字列pと文字列rをk連結した結果である場合、pをqのprefixと呼ぶ。rは空文字の可能性もあり、任意の文字列はそれ自身のprefixである。)
17. xの値がyの値のprefixである場合、trueを返して終了。
18. xとyの文字列を順に見ていき、最初に異なる文字が現れる位置kを見付ける。(両者がそれぞれのprefixではないので、そのような数kは必ず存在する。)
19. xのk番目の文字のコードポイントの値をmとする。
20. yのk番目の文字のコードポイントの値をnとする。
21. m < nであればtrueを返す。それ以外ならfalseを返す。

今回の文null > 0で考えてみよう。
ステップ1と2で、nullと0それぞれにToPrimitiveを呼んでプリミティブ型に変換しようとしている。
ToPrimitiveの結果は以下のようになる。

入力の型 出力
Undefined 変更なし
Null 変更なし
Boolean 変更なし
Number 変更なし
String 変更なし
Object デフォルト値を返す。Objectのデフォルト値はobject型の内部メソッドDefaultValueによって決められ、第2引数は無視される。

変換テーブルに従うと、nullも0もどちらも変換されない。
よってステップ3には当てはまらず、ステップ4と5で両辺がNumberへと変換される。
Numberへの変換は以下の表のようになっている。

入力の型 出力
Undefined NaN
Null +0
Number 変更なし
Boolean trueなら1、falseなら+0
... ...

StringとObjectは今回無関係であるため省略した。
詳細はここから確認できる。

nullは+0となり、0は0のままになる。
どちらの値もNaNではないのでステップ6と7はスキップする。
ステップ8で+0と0は等しいと評価され、アルゴリズムはfalseを返すこととなる。
以上より、

    null > 0; // false
    null < 0; // false

両方ともfalseになる。

The Abstract Equality Comparison Algorithm

次に進もう。

    null == 0; //false

これは興味深い結果だ。
==演算子はAbstract Equality Comparison Algorithmというアルゴリズムに従い、truefalseを返す。

1. xとyの型が異なる場合、14に進む。
2. xの型がundefinedであればtrueを返して終了。
3. xの型がNullであればtrueを返して終了。
4. xの型がNumberでない場合、11に進む。
5. xがNaNであればfalseを返して終了。
6. yがNaNであればfalseを返して終了。
7. xとyが同じ数値であればtrueを返して終了。
8. xが+0でyが-0であればtrueを返して終了。
9. xが-0でyが+0であればtrueを返して終了。
10. falseを返して終了。
11. xの型がStringである場合、xとyの文字列が全く同じであればtrueを返して終了。それ以外ならfalseを返して終了。
12. xの型がBooleanである場合、xとyの両方がtrueか、両方がfalseであればtrueを返して終了。それ以外ならfalseを返して終了。
13. xとyが同じObjectを参照しているか、結合オブジェクトである場合、trueを返して終了。それ以外ならfalseを返して終了。
14. xがnullでyがundefinedであればtrueを返して終了。
15. xがundefinedでyがnullであればtrueを返して終了。
16. xの型がNumberでyの型がStringの場合、x==ToNumber(y)の結果を返して終了。
17. xの型がStringでyの型がNumberの場合、ToNumber(x)==yの結果を返して終了。
18. xの型がBooleanの場合、ToNumber(x)==yの結果を返して終了。
19. yの型がBooleanの場合、x==ToNumber(y)の結果を返して終了。
20. xの型がStringかNumberでyの型がObjectの場合、x==ToPrimitive(y)の結果を返して終了。
21. xの型がObjectでyの型がStringかNumberの場合、ToPrimitive(x)==yの結果を返して終了。
22. falseを返して終了。

nullと0の比較では、まず型が異なるためステップ1からステップ14に飛ぶ。
nullの型はNullなので、ステップ14から21までは全て当てはまらない。
最終的にステップ22まで進み、結果はデフォルト値のfalseとなる。
従って、

    null == 0; //false

となる。

The Greater-than-or-equal Operator (>= )

最後に問題のこれをチェックしていこう。

    null >= 0; // true

これは仕様による理解を完全に放棄したところだ。

高度に政治的な判断によると、関係演算子>=は以下のように評価される。

    null < 0 がfalseであればnull >= 0 がtrueになる

Really?

従って、

    null >= 0; // true

実のところ、この結果は理に叶っている。
数学的には、2数xyがあって、xがyより小さくないのであれば、xはyより大きいかyと同じでないといけないからだ。

この式の評価は、最適化のため以下のように行われたのではないかと考えている。
まずx>yを評価する。
これがfalseだった場合、次にx==yを評価する。
それでもなければ最後にx<yを評価しなければならないところだが、x>yの結果から即trueを返しているのではないか。

( >=演算子の正確な評価は、ここで見付けることができる )

・・・

このように些細な疑問でも、答えを調べていく過程でいくつもの新しい発見がありました。
この記事が貴方の助けになることを願っています。

・・・

もっと助けが必要?
興味があるならTwitterGithubで活動しています。

まとめ

>=の評価の項目には、
x<yがtrueかundefinedならfalseを返し、それ以外ならtrueを返す】
つまりx<yx>=yは反対の値になる、と書かれているように見えるのだが違うのだろうか。

    null > 0;  // false
    null == 0; // false
    null < 0;  // false

    null >= 0; // true
    null <= 0; // true

Oh.