thisにセットされるもの

@__gfx__さんがhttp://d.hatena.ne.jp/gfx/20120223/1329996834:JavaScriptのthisの扱いが難しすぎる件という記事を書いていたので見てみました。

いろいろなケースでthisに何がなぜ設定されるのかが分からない、というものです。以下転載。

var o = {}, tmp;

o.f = function() { console.log(this.toString()) };

o.toString = function() { return "o" };
o.f.toString = function() { return "o.f" };
(function(){ return this })().toString = function() {
    return "global";
};


o.f(); // o
(o.f)(); // o

([o.f][0])(); // o.f
(new Array(o.f)[0])(); // o.f
([tmp  = o.f][0])(); // o.f

(tmp = o.f)(); // global
(function(){ return o.f })()(); // global

({x: o.f, toString: function(){ return "t" }}.x)(); // t

JavaScriptのエキスパートではない僕にも当然分からなかったので、@bulkneetsさんや@kazuhoさんのツイートを足がかりに考えてみました。

いろいろ考えたり試したりした結果こうであろうと考えたルール:

  1. 「o.f()」のようにオブジェクトを明示した上でオブジェクトのプロパティにセットされている関数を呼び出す場合、thisにはそのオブジェクトがセットされる。
  2. オブジェクトを明示せずに関数を呼び出す場合、thisにはグローバルオブジェクトがセットされる。
  3. 配列aについて、a[n]はaの"n"というプロパティを指す。
  4. 「(o.f)()」のように、関数を表すものが括弧で囲われている場合、その括弧は(thisを決定する上では)無視される。

JavaScriptのエキスパートから見ると回りくどい言い方になっているかもしれませんがご容赦下さい。)

これらのルールを使って説明していきます。

詳細な解説

o.f(); // o

ルール1よりthisにはoがセットされる。

(o.f)(); // o

ルール4より(o.f)の部分はo.fと同じと考えてよい。よってthisにはoがセットされる。

([o.f][0])(); // o.f

ルール4より([o.f][0])の部分は[o.f][0]と同じと考えてよい。またルール3よりこれは[o.f]という配列オブジェクトの"0"というプロパティを指す。さらにルール1よりthisには配列オブジェクト[o.f]がセットされる。

…となるはずなのに出力結果がo.fなのはどうして!?としばらく悩みましたが、実はo.fという出力はo.f.toString()によって出力されるのではなく配列[o.f]によって出力されるということに気づきました。通常配列のtoString関数は配列の中身をカンマ区切りで表示するため、要素がo.fだけの配列については出力がo.fとなるのでした。これについては@__gfx__さんも追記として書いていますが、@bulkneetsさんが

と書いているとおりです。実際に試してみると、

([o.f, "a"][0])();

はo.fではなくo.f,aとなりました。

(new Array(o.f)[0])(); // o.f

一つ前のケースと同様の考え方でthisには配列オブジェクトnew Array(o.f)がセットされる。

([tmp  = o.f][0])(); // o.f

一つ前のケースと同様の考え方でthisには配列オブジェクト[tmp = o.f]がセットされる。また、式tmp = o.fの評価結果はo.f(の指すもの)なのでthisには[o.f]がセットされる。

(tmp = o.f)(); // global

(tmp = o.f)の評価値はo.fの内容になるが、この式ではオブジェクトが明示されていない。よってルール2よりthisにはグローバルオブジェクトがセットされる。

なおこれは一見(o.f)()と同じように見えるのですが、@kazuhoさんが

とおっしゃっていたとおり、o.fとtmp = o.fは評価値こそ同じですが式自体の意味は全く違います。例えばo.fは左辺値となり得ますがtmp = o.fは左辺値にはなり得ません。そのためo.f = 1はOKですが(tmp = o.f) = 1はエラーになります。

(function(){ return o.f })()(); // global

ルール2よりthisにはグローバルオブジェクトがセットされる。

({x: o.f, toString: function(){ return "t" }}.x)(); // t

ルール4より({x: o.f, toString: function(){ return "t" }}.x)の部分は{x: o.f, toString: function(){ return "t" }}.xと同じと考えてよい。またこの式ではオブジェクト{x: o.f, toString: function(){ return "t" }}が明示的に指定されているのでルール1よりthisには{x: o.f, toString: function(){ return "t" }}がセットされ、その結果このオブジェクトが持つtoString関数が呼び出されてtが表示される。

おまけ。@__gfx__さんが再追記で書いている(o.f=o.f)()がglobalになったことの解釈ですが、ルール4より(o.f=o.f)はo.f=o.fと同じと考えてよく、またo.f=o.fの評価値はo.fの内容になり、かつオブジェクトが明示されていないため、ルール2よりthisにはグローバルオブジェクトがセットされることになります。

まとめ

今回の件は

でほぼ完全に説明し尽くされているといって良いと思います。よって

@kazuhoさんと@bulkneetsさんはすげー

が結論となります(笑)。