2012年3月11日日曜日

JavaScript の this のキモチ

JavaScript(というか CoffeeScript)を勉強していて、this が難しいなぁと思っていました。いろいろ読んでも、「this は、こういう場合にはこれを指す」とは書いてあるんだけど、なぜそうしてあるのかが分からず、どうにも理解できなかった。例えばこのへん:
でも規格(上のひとつめ)としばらく格闘して、何となくキモチがつかめた気がするので、この難しさ感を忘れないうちに書いておこうと思います。まだ JavaScript 初中級者くらいなので間違いがあるかも知れません。

普通のオブジェクト指向の考え方では、オブジェクトは内部変数と手続きを持っていると考えます。下の図で、a がオブジェクト、x が内部変数、m が手続きです。この手続きをメソッドと呼びます。


で、オブジェクト指向の基本的な考え方に、「内部変数はそのオブジェクトが持つメソッドからのみアクセスする」ってのがあります。a の内部変数 x にアクセスするには、a のメソッドのどれか(ここでは m しかないけど)の中から this.x とするしかない、ってことです。これをカプセル化と呼びます。カプセル化は不便に見えるかも知れないけど実は便利な機能です。

さて、JavaScript においては、メソッドは関数として定義される。そして関数は独立したひとつのオブジェクトです。以下のようなコードを書くと…
var a = { x: 0 };
function f() { this.x++; }
a.m = f;
こうすると関数 f が、オブジェクト a のメソッド m として呼び出せるようになります。ここで、
a.m();
とすれば、a が持つ内部変数(属性、property)x の値が1増えます。

この時の a と f の関係を描くと下の図のようになっています。


ここで気づくのは、(関数である)オブジェクト f は、他のオブジェクト a の内部変数をいじる!ってことです。オブジェクト指向はカプセル化だよね〜とか思っている私のように古いタイプ(?)の人間は、this.x と書かれたらまさか他のオブジェクトの内部変数をいじってるとは思わないので、これを理解するというか納得するのに時間がかかりました。(いや、分かってしまえば、「関数がメソッドになる」「関数は独立したオブジェクトだ」とはそういう意味なんだけれども…)

というわけで私と同類な人向けに:JavaScript の this はカプセル化じゃありませんよ!

じゃあ this の意味は何か。同じ関数 f を別のオブジェクト b のメソッドにもしてみると分かりやすい。
var b = { x: 0, n: f };

これで関数 f は、a.m() としても呼べるし、b.n() としても呼べるようになりました。f のコード中にある this.x は、a.m() として呼ばれた時には a の中の x を意味し、b.n() として呼ばれた時には b の中の x を意味します。

と言うか、そういう意味になるように、this が指すものを、関数の呼び出し方によって変えているのです。a.m() として呼ばれたコードの実行中には this は a を指し、b.n() として呼ばれたコードの実行中には this は b を指す。

再び同類向けに:this は「このオブジェクト」ではなく、「この関数が、あるオブジェクト a のメソッド m として与えられており、なおかつ a.m() の形で呼ばれた場合、a」という意味。長い。てか全然 this じゃない。

(この関数をメソッドとして呼んだオブジェクト、と言いたいところだけど、そうでもない。a.m() を呼ぶのは a ではなくて、そのコードが書かれた何か(関数とかグローバルなコードとか)なので…)

だから、こんなコード
var o1 = {
  x: 0,
  m: function() {
    this.x++;
  }
};
を書いて、「これでこの関数が呼ばれる時には必ず o1 の属性 x が増える」と安心してはいけないのです。たしかに o1.m() で呼ばれるとそうなる。でもここで、
var o2 = {
  x: 0,
  m: o1.m
};
o2.m();
とすれば、同じく o1 の「中に書いてある」(ように見える)関数が実行されるけど、増えるのは o2 の方の x。この時の状況はこう↓(上の a と b の場合と同様)


この関数は o1 の「中で」定義されているわけではなく、そのオブジェクトに所属してもいません。this が何を指すかは、あくまで「呼ばれ方」(o1.m なのか o2.m なのか)で決まります。(私が正しく理解していれば。)

こういう動作を見て、私は、ああ JavaScript は普通のオブジェクト指向言語の this をエミュレートしたかったんだなと理解しました。それが this の役割なんだろうと。

こう考えると、
  1. 関数 f が裸で f() のように呼ばれたら this はグローバルオブジェクト(とか undefined)を指す
  2. (a.m = a.m)() として f を呼ぶと、this は a を指さずにグローバルオブジェクトとかを指す
  3. 関数のメソッド .call や .apply で this を明示的に指定できる
  4. 関数 F を new F のように呼ぶと、F のコード中で this は新しくできたオブジェクトを指す
なども、なるほどと思えるようになりました。私が感じるキモチはこう↓
  1. この場合、f をメソッドとして呼んでないけど、this には何かを指させなきゃいけない。ならば undefined か null かグローバルオブジェクトだが、まあ何でもいいのでは。
  2. (a.m = a.m) の右辺の式の値として、生の f が出てくる。この値(関数そのもの)を使って呼んでるからメソッドとして呼んでない。よって1と同じ。
  3. メソッドがオブジェクトに所属する普通のオブジェクト指向とは違って、JavaScript では関数が自由にどんなオブジェクトのメソッドにもなれる(a.m = f として a.m() と呼ぶ)。でもそれが自由なら別にわざわざ a.m = f として属性にすることなく a のメソッドとして呼べてもいい(f.apply(a))。
  4. 新しくできたオブジェクトを o とし、そのオブジェクトが初期化関数を表す属性 init を持ちそれが F を指しているとして、o.init のように呼ばれたと考えれば、普通のオブジェクト指向言語と同じ。
とは言え、
var g = function() { this.x++; };
g.x = 0;
として
g();
が自分(関数 g)の x を増やさないのは何とも…(ってまだ言ってる。それは this じゃないんだって。)

まとめ。
  • this は「このオブジェクト」と思わない方がいい。
  • this は「メソッドとして呼ぶ呼び方でこの関数が呼ばれた時の、指定されたオブジェクト」を意味する。
  • this は(この関数とは)別のオブジェクトを指す。何を指すかは、呼び出し毎に決まる。
この難しさの原因は、やっぱり関数を独立したオブジェクトにしたためで、JavaScript にとってはもしかしたらオブジェクト指向よりもそっちの方が大事だったのかも知れないなぁと思った。オブジェクト指向の方がおまけなのかも。