驚きいっぱいのJavaScript?

言語やインタフェースの設計には「驚き最小の原則」というのがある。まつもとさん本人はそんなこと言っていないようだが、かつて、Rubyはその原則に沿った言語と言われていた。一方、JavaScriptはそれに反する言語と未だに見なされているようだ。多くの場合、よく理解していないのが原因である。理解した上でも、やっぱりおかしいよ、というのもあるかも知れないが、じゃ、その場合どう定義したらよいんだ、というのはいろいろ難しい問題がある。

wtfjs(http://wtfjs.com/)にはJavaScriptのそんな「変な挙動」が集められている。wtfなんてタイトルをつけているくらいなので、あまり真面目に見る必要はないのかも知れないけれど、主なものについて古い順から軽く解説してみた。ちなみに、wtfはWhat The F*ckの略。


typeof NaN === 'number' // true
Infinity === 1/0        // true
0.1 + 0.2 === 0.3       // false

wtfなのは最後のものだろうけど、0.1+0.2が0.3になる言語の方が少ないと思う。NaN はNot a Numberの略だけど、型としては数値でも全然不自然ではないし、1/0はC言語でも浮動小数のときはINFINITYになるし、例外かfalseにした方がよいっていうことなのだろうか?


(x=[].reverse)() === window     // true

一見不思議だけれど、全然おかしくない。しかし、規格を知らないとwtfと思われても仕方ない。同じ意味にちょっと書き換えると以下のようになる。

Array.prototype.reverse.call() === window

Arrayのreverse関数はジェネリックであり、引数がArrayでなくてもよいのであるが、ここではcall()の引数が省略されているので、グローバルオブジェクトが代入されている。グローバルオブジェクトはブラウザの中ではwindowにバインドされている。windowのlengthプロパティは0になっているため、reverseは引数そのものを返す。従って、引数のwindowオブジェクトがそのまま返るだけのこと。

ただし、ECMAScript5ではcall()の第1引数を省略したときグローバルオブジェクトが渡されるという仕様は変更されているので、上記は成立しないと思われる。


NaN === NaN    // false

これはJavaScriptに限ることではなく、NaNの定義といってもよい。NaNは自分自身を含めてすべての数値と等しくなることはない。それがNaNかどうかはisNaN()という関数を使う必要がある。


alert(111111111111111111111); // alerts 111111111111111110000

JavaScriptの数値表現はIEEE754 doubleであり、もっとも大きな整数は9007199254740991だ。これより大きな数は精度が落ちるため正確には表現できない。JavaScriptは残念ながらBigNumには対応していない。


parseInt('06');  // 6
parseInt('08');  // 0
// This is because parseInt accepts a second argument for radix. If it is not
// supplied and the string starts with a 0 it will be parsed as an octal number. 
// Riiiiiiight, of COURSE. 

強調してRightと言うことではない。それは10年以上前の仕様。ECMAScript3では0で始まる文字はradixが省略された場合には、8進数か10進数かは実装依存でどちらでもよいが、10進数を推奨している。一方、ECMAScript5では完全に10進数となっているので、parseInt('08')は8になる。現状、Operaがこの動作になっている。


typeof null             // object
null instanceof Object  // false

typeof nullがobjectなのは僕も抵抗を感じるが、それ以外の適切な型が定義されていないので仕方ない。nullはprimitiveであり、primitiveはObjectのインスタンスではない。


[] == false;    // true
"" == false;    // true
null == false;  // false, that's more like it

問題は最後のもの。==はどちらか一方がBooleanのときはそれを数値にしてから、再帰的に==を実行するので、null==falsenull==0となってこれがfalseになる。nullはprimitiveでありnullと==なのはnull自身とundefinedだけ。

プログラマとして気をつけるべきことは、よく、if文などで下記のようなコードを見かけるが、この2つのifは等価ではないということ。C言語のコーディング規約でx==NULLと明示的に書きましょう、なんてのを何も考えずにJavaScriptにも適用したのかも知れないが危険である。

if (x == false) {
 ...
}
if (!x) {
 ...
}


[] == ![]       // true

![]はArrayをprimitiveなbooleanにしてから!(not)する。ArrayオブジェクトをToBooleanするとtrueだから、![]はfalseになる。[]==false==の両辺の型が異なっており、どちらかがBooleanならばそれをToNumberして再帰的に実行するので、[]==0となる。さらに、どちらか一方が数値ならもう片方もToPrimitveするので[]をToPrimitiveすると0だから0==0で結局trueとなる。


(function(){
   alert(window); // "undefined"
   var window = window;
})();

これは、関数の中のローカル変数のスコープは関数全体であり、どこで宣言しようが、その変数はどこでも使えるということ。これは下記と同じ。

(function(){
   var window;
   alert(window); // "undefined"
   window = window;
})();

グローバル変数のwindowをローカル変数で隠すので、当然windowはundefined。


Object.prototype.foo = 10;
console.log(foo);               // 10

グローバル変数というのはグローバルオブジェクトのプロパティであり、グローバルオブジェクトはObjectのインスタンスなので当然の結果。


// note the number to the left of the 'e', 7 and 8 respectively
alert( 1.7976931348623157e+308 === 1.7976931348623158e+308 ); // true!

IEEE754から期待される結果。有効精度の問題。関連エントリー2を参照。


(1) === 1; // true
Number.prototype.isOne = function () { return this === 1; }
(1).isOne(); // false!
Number.prototype.reallyIsOne = function () { return this - 1 === 0; }
(1).reallyIsOne(); // true

この場合thisはNumberクラスのインスタンスであるオブジェクトがバインドされているんだからthis===1は当然falseです。reallyIsOneはthis-1===0よりは +this===1の方がスマートと思う。でも、そうだったら、this==1とした方がよい。


++[[]][+[]] === 1       // yay! wtf!

これはなかなかの傑作。シンタックスエラーの回避とオブジェクトのToPrimitive変換、および左辺式の条件をうまく利用している。
++[""][0]が1になるのと同じ。


JavaScriptはブラウザで簡単に使えるためか、初めて覚えたプログラム言語という人が多いのかも知れない。そのためかどうかわからないが、他の言語でもあるような初心者が陥り易い罠が必要以上に悪い評判として取りざたされている。しかし、ECMA規格として承認されているプログラミング言語としては厳密に規定されているものであり、全ての挙動はその規格から正確に説明できると思ってよい。

JavaScript界のヨーダことCrockfordがThe World's Most Misunderstood Programming Languageというインパクトのある記事を書いてからもう9年にもなる。いろいろな意味で、まだまだ誤解は解けないようだ。

関連エントリー