JavaScriptのcall.applyの不思議 (wtfjsを解説)

wtfjsに下のような面白いものが登録されました。

alert.call.call.call.call.call.apply(function (a) {return a}, [1,2]) // 2

確かに、どうしてこうなるのかは直ぐには分かりません。一つ一つゆっくりと確認してみましょう。

関数コールのobj.func(arg,...)という形式は、obj.func部分の評価結果は参照型(reference type)となりますが、obj部分の評価結果は値になります。(詳しくは関連エントリ[2]をご覧ください)

なので、

func1.func2.func3(arg,...);

という形式は、func1.func2部分を評価して結果を変数に代入しても結果は変わりません。

つまり、下記のようにしても動作は同じです。

var fn = func1.func2;
fn.func3(arg,..);

そこで、問題をちょっと変形すると次のようになります。

var fn = alert.call.call.call.call.call;
fn.apply(function (a) { return a }, [1, 2]);

変数fnには何が代入されるかを考えてみます。まず、alert.callの評価結果の値は何かというと、alertは関数オブジェクトであり、任意の関数オブジェクトはFunctionオブジェクトのインスタンスなので、Function.prototype.callになります。Function.prototype.callも関数オブジェクトなので、そのcallプロパティもFunction.prototype.callになります。つまり、

Function.prototype.call.call.call.call === Function.prototype.call; // true

などが成立します。

なので、変数fnを書き換えると以下のようになります。

var fn = Function.prototype.call;
fn.apply(function (a) { return a }, [1, 2]);

ここまできてもちょっとややこしいのですが、ほんのちょっとだけさらに簡単にすると、obj.apply(thisArg, [a1, a2, ...])obj.call(thisArg, a1, a2, ...)と同じなので、下のようになります。(簡単になったかどうかは好みの問題かも知れませんが)

var fn = Function.prototype.call;
fn.call(function (a) { return a }, 1, 2);

そこで、問題は、

obj.call.call(thisArg, a1, a2, a3, ...)

がどういうことかを考えればよいことになります*1。これは、obj.call関数を関数として実行する際に、thisにthisArgをバインドし、obj.call関数の引数にa1, a2, a3,...を渡すということです。

obj.call関数はFunction.prototype.callであり、この関数のthisは実行する関数自身ですから、次のものと同じになります。

thisArg.call(a1, a2, a3, ...)


結局、問題のコードは、

(function (a) { return a }).call(1, 2);

を実行したことと同じであり、2を返すということになります。


こういうおかしなコードを思いつくってスゴイですね。似たようなものがないかとちょっと考えましたが面白いものは思いつきませんでした。

関連エントリー

*1:applyとcallについては「applyとcallの使い方を丁寧に説明してみる - あと味」に分かりやすい解説があります。