読者です 読者をやめる 読者になる 読者になる

MANA-DOT

PIXEL ART, PROGRAMING, ETC.

JavaScriptのthisの出現パターンは2つしか無いという話

前にJavaScript初心者向けにthisについて言及しました。当時わかりやすく書いたつもりだったけど、今読み直すとわかりづらい・・・ので、視点を変えて書きなおしてみようという試み。

JavaScriptのthisの決まり方

JavaScriptで関数中でthisと書いた時、thisが何になるかはその関数の呼び出し方によって以下のように決まります。

  • obj.method() のようなメソッド呼び出しの場合、obj (ちなみにhoge.fugaのhogeをレシーバと呼んだりする)
  • new obj() のようなコンストラクタ呼び出しの場合、コンストラクタが生成する新しいオブジェクト
  • func() のような、レシーバ不在の普通の関数としての呼び出しの場合は、グローバルオブジェクト
  • Function.protoype.apply メソッドを利用すると、thisを任意に指定してあげる事ができる。例えば、obj.method.apply(hoge)という呼び出しでは、hogeがthisになります。

このことは、例えば次のようなコードで確かめられます。

// コンストラクタ呼び出しのテスト
var Person = function(name) {
    this.name = name;
};
var alice = new Person("Alice");
console.log(alice.name);  // 確かに、コンストラクタ中のthisは新しく生成されたaliceであることがわかる。

// メソッド呼び出しのテスト
alice.printName = function() {
    console.log(this.name);
};
alice.printName(); // 確かに、メソッド呼び出し形式ではthisがaliceになっていることがわかる。

// 代入して、メソッド形式でない普通の関数呼び出しにしてみる。
var printName = alice.printName;
name = "Bob"; // グローバルにnameという名前で変数を定義してみる。
printName();  // レシーバのない関数形式ではthisがグローバルに向いてしまっていることがわかる。もしグローバルにnameがいなければ、エラーになってしまうでしょう。

// applyすれば、好きなオブジェクトをthisにできる。
printName.apply({ name:"Carroll" });
alice.printName.apply({ name:"Carroll" });

上の例のように、関数がどう呼び出されるかを見ればthisは一目瞭然なのですが、慣れてないといまいちピンとこないのかなと思います。 それもそのはずで、関数本体を書いていてそこにthisを書いているときは、当たり前なのですが、その関数呼び出しは書いていないのです。 JavaScript初心者が惑わされるのにはこういう点もあるのかなと思います。

2つのthis出現パターン

しかしJavaScriptのthisには実は基本的に2つの出現パターンしかありません。(少なくとも、thisがわからないと感じる程度の初心者が書かなければならない場面では) 実際に関数を書いていてそこにthisを書いている時に、今自分はどちらのつもりで書いているのか考えれば、どう呼び出されるかも明らかです。 それぞれくわしく説明していきます。

オブジェクト指向言語的なthis

一般的なオブジェクト指向言語的なthisの使い方、obj.method()でレシーバを取得するための使い方です。 典型的なのは、

var alice = { name: "Alice" };
alice.printName = function() {
    console.log(this.name);
};
alice.printName();

のような予めプロパティが定義してあるようなオブジェクトに対してそのプロパティを参照するための使い方です。これではそこまで有り難みが感じられないかもしれませんが、よりオブジェクト指向的に

var Person = function(name) {
    this.name = name;
};
Person.prototype.printName = function() {
    console.log(this.name);
};
var alice = new Person("Alice");
var bob = new Person("Bob");
alice.printName();
bob.printName();

とした場合、有り難みは一目瞭然です。複雑な構造を使う場合、こういったクラス的なオブジェクト指向プログラミングをする事になると思います。この時、thisを書く理由は明確で、obj.method形式で呼び出された時のobj、メソッドのレシーバを参照するためです。書いている時はthisが何を指すことになるのか混乱することはあまり無いでしょう。

このようなthisの使い方をするのは、あるオブジェクト(prototypeも含む)のプロパティとして関数を代入している場合です。 コンストラクタの場合は特殊ですが、付近にメソッドを定義している箇所があるはずなので、見分けるのに苦労はしないでしょう。

ライブラリの機能として提供されるthis

もう一つ、JavaScriptでthisを書く場面があります。それは、ライブラリの機能としてthisの使用をする場合です。

例えば、jQueryclick関数では、引数に渡したコールバック関数内でのthisは、イベントを発火させたDOMオブジェクトを指しています。

$("p").click(function () {
    $(this).slideUp();
});

これは、jQueryというライブラリの中で、上述のFunction.prototype.applyを用いてこのような挙動になるようにthisを設定してあげているわけですが、ライブラリの使用者としてはそれを意識せずに、ただ「引数として渡したコールバック関数内のthisで、イベントを発火させたDOMオブジェクトを得られる」というライブラリの使用を把握しておけば良いです。 逆に、仕様をよく知らないライブラリだと、わからん殺しに陥ることも少なくないです。

このパターンのthisは、基本的ライブラリの関数の引数としてコールバック関数を渡している時に現れます。なので、そういうthisを見つけたら、ライブラリ関数の仕様を読んでみるといいでしょう。 もしも、コールバック関数を先に定義しておいてあとからライブラリ関数に渡すようなコードだと、多少見分けるのに苦労するかもしれません。

var callback = function () {
    $(this).slideUp();
};
$("p").click(callback);

落ち着いて、関数がどのように用いられるかを探してみましょう。

それ以外のthis

基本的には上記以外のパターンでthisを使うことは、少なくともJavaScript初心者のうちは無いと思われます。まず第一にミスを疑ってみるといいでしょう。

まとめ

JavaScriptのthisを書くパターンとして以下の2つを挙げました。

前者の場合は関数はコンストラクタまたはメソッドとして用いられ、thisはメソッドのレシーバとなります。 後者の場合は、関数はコールバック関数として、ライブラリ関数の引数として用いられます。thisが何になるかはライブラリの仕様書にきっと書いてあります。 このことを頭に入れておくと、thisで悩むことは減るのではないでしょうか。