MANA-DOT

PIXEL ART, PROGRAMING, ETC.

JavaScriptのthisについて

同期の間でJavaScriptのthisが難しいって話をちょくちょく聞いたので。簡単に説明してみる。 間違っているところがあったら是非突っ込んでほしい。

※サンプルコードにミスがあったため、修正を行った(9/5) 続きを書いた(9/20)

なぜthisが必要なのか

まず、thisうんぬん言う前になぜthisがJavaScriptに必要なのか、thisあると何が嬉しいのかを理解してないと、thisの挙動を説明されてもピンと来ないと思う。オブジェクト指向プログラミングを理解していればなんてことはないのだろうけど、念のため再確認しておきたい。

オブジェクト指向プログラミングでは、ある性質を持つモノや概念などを、オブジェクトとして扱うことで効率的なプログラミングを行うのが大前提である。モノや事象の状態をフィールド(ないしはプロパティ)として、ふるまいや処理をメソッドとしてオブジェクトに関連付けてあげる事で、一連のまとまった性質として扱ってあげる事ができる。

僕の大好きなゲームプログラミングを例にする。ゲームのプレイヤーの位置情報(x座標・y座標)をオブジェクトで表現するとすると、JSなら次のようにするのが最も簡潔である。

var player = {x:0, y:0};

ここで、このゲームではplayerを斜め45度の方向に1マス進ませることが頻繁にあったとする。頻繁に呼ばれるので、この処理は関数化したい。

var player = {x:0, y:0};
var walkDiagonal = function(gameObj) {
    gameObj.x++;
    gameObj.y++;
}
walkDiagonal(player);

これで、頻繁に利用される斜めに移動する処理を関数化できた。これだけでもよさそうだが、オブジェクト指向プログラミング的にはこの関数はplayerオブジェクトのメソッドとして、一箇所にまとめてしまいたい。気分としては

var player = {
    x:0,
    y:0,
    walkDiagonal:function(gameObj) {
        gameObj.x++;
        gameObj.y++;
    }
};

こんな感じだ。これでwalkDiagonalはplayerのメソッドとなった。しかしこのコードには一つ残念な点がある。それは、引数としてgameObjすなわちplayer自身を渡してあげなければならないということだ。

player.walkDiagonal(player);

別にこれでも構わないという人もいるかもしれないが、少し残念である。文脈上、明らかにplayerがwalkDiagonalするということが読み取れるので、

player.walkDiagonal();

という風に記述できると嬉しい。些細な事ではあるが、オブジェクトのメソッドでは、walkDiagonalのようにその「メソッドを持つオブジェクト」自身を参照することが頻繁にあるため、このように引数のオブジェクト自身を省略できるとありがたい。もしかしたら引数の方の値を修正し忘れるだとか、そういったミスもあるかもしれないし、何よりせっかくのオブジェクト指向プログラミングが台なしである。もうほとんど答えは出ているが、この「メソッドを持つオブジェクト」をメソッド中で参照するための特別な変数がthisなのである。

var player = {
    x:0,
    y:0,
    walkDiagonal:function() {
        this.x++;
        this.y++;
    }
};
player.walkDiagonal();

なるほど、これで引数でわざわざわたさなければならなかった「メソッドを持つオブジェクト」をわざわざ引数に書く必要はなくなった。この適当な説明でthisがなぜ必要なのかなんとなく理解できてもらえたら、実際thisが何であるのかについてすんなり理解できると思う。もしこの説明でthisの必要性が理解できていなかったらば僕の責任なので、遠慮無く質問して欲しい。

JavaScriptのthis

では、JavaScriptのthisは何なのか。端的に言うと、さっき説明した通り「メソッドを持つオブジェクト」を参照できる変数である。この「メソッドを持つオブジェクト」というのは、

obj.method();

というコードがあった場合のobjのこと。これだけである。どんなに複雑な形式であったとしてもこのルールは変わらない。例えば、

hoge.foo.bar.method();
hoge.method1().method2();

のような複雑な形式だった場合、ちょっと悩んでしまうかもしれない。でも、次のように考えてあげればすぐにthisは何か理解できると思う。

(hoge.foo.bar).method();
(hoge.method1()).method2();

それぞれのメソッドの実行中でのthisがなにかわかっただろうか。

このようにJavaScriptのthisはわかってしまえば非常に簡単なのだが、一つだけ例外がある。それはobj.method()の形式にならない唯一の関数呼び出しの場合である。それは、

func();

のような、グローバル関数の呼び出しである。この場合、func中でのthisはグローバルオブジェクト(グローバル関数・変数が所属するオブジェクト)となる。これに関しては仕様であると納得してもらうしかないが、僕はグローバル関数はグローバルオブジェクトに属しているので、グローバルオブジェクトがthisになるのは妥当であると解釈している。

ちなみに、他の言語のthisに慣れてしまっていると、やっぱりJavaScriptのthisが難しいと思うことがあるらしい。例えば、

var hoge = {
    text: 'ほげ',
    method: function() {
        return this.text;
    }
};
var myMethod = hoge.method;
myMethod(); // undefinedが帰ってきてしまう!!"ほげ"じゃない!おかしい!

というコードを書いた時、func中でのthisがhogeにならない!なぜだ!と思ってしまうことはないだろうか。でも先程のルールに当てはめて考えて欲しい。func()はそもそもobj.method()の形式になっておらず、グローバル関数としての呼び出しである。このとき先ほどの例外に当てはまり、thisはグローバルオブジェクトとなってしまうのだ。JavaScriptのthisはあくまでも呼び出し時の形式で決まることに注意して欲しい。こういったミスはよく次のような場面で見かける。

document.onload = hoge.method;
// onloadに代入されるのはmethod単体で、hogeの情報は一切引き渡されていない!
// もしhogeの情報も渡したいのなら
// document.onload = function() { hoge.method(); };

さて、思ったより長くなってしまって簡単であると言いつつ説明は難しくなってしまったかもしれない。 けれども概念としてはとても簡単であることは伝わったと思う。 個人的にはこういった言語の概念を理解するには、やはりコードを書いて体で覚えるのが一番であると思っているので、失敗を恐れずにコードを書いていくのがいいと思う。