tacamy.blog

JavaScriptを勉強中の人のブログです。

JavaScript の this を理解する

this はインスタンス自身を指す、ただそれだけの話でしょう?
そんなふうに考えていた時期が私にもありました。そう、JavaScript の this に出会うまでは・・。

用語について

私は Java 脳で書いてるので、言葉遣いが JavaScript と若干違う部分があると思います。 下記のようなイメージで言葉を使っています。

用語 意味
クラス インスタンスオブジェクトの元となる設計図
コンストラクタ クラスのコード部分で、new したときにコンストラクタの内容で初期化する
インスタンス クラスを元に実体化したオブジェクト
メンバ変数 クラスやインスタンスに属するローカル変数
メソッド クラスやインスタンスに属する命令(関数)

オブジェクト指向とプロトタイプと this

JavaScriptオブジェクト指向開発では、元になるクラスを new することで、インスタンスを生成します。

同じクラスを元に作られたインスタンス間において、メンバ変数の値はインスタンスにごとに異なり、メソッドは共通で使える場合が多いです。そのため、基本的にはメンバ変数はコンストラクタ部分に、メソッドはプロトタイプ部分に記述します。 (詳しい内容は プロトタイプチェーンをもっと理解する を参照。)

function Bird(voice) {
    this.voice = voice;
}
Bird.prototype.sing = function() {
    console.log(this.voice);
}

var cock = new Bird("cocka-a-doodle-doo");
cock.sing();  // > "cocka-a-doodle-doo" (this が cock を指している)

var duck = new Bird("quack");
duck.sing();  // > "quack" (this が duck を指している)

上記のコードでは、2 箇所に this を使っています。

  • Bird クラスのコンストラクタthis.voice = voice;
  • Bird クラスのプロトタイプ sing メソッド内 console.log(this.voice);

それぞれの this の使われ方について見ていきます。

メンバ変数に値を代入する this

Bird クラスをインスタンス化するときに引数で渡した値を、this を使ってインスタンス自身にセットすることにより、そのインスタンスのメンバ変数として値を代入できます。 このメンバ変数は、インスタンスごとに個別に作られ、それぞれ別々のメモリ領域に保管されます。

このとき this はインスタンス自身(cock, duck)を指しており、それぞれのインスタンスで voice 変数を持っています。

メソッドからメンバ変数を参照する this

クラスのプロトタイプで定義されたメソッドは、そのクラスを元にして作ったインスタンスから実行できます。しかし、実際にはメソッド本体はひとつしかなく、そのメソッドのあるアドレス(番地)を参照しているだけで、インスタンス化するときにメソッドが複製されているわけではありません。

sing メソッドが定義されているのは Bird クラスですが、「this はインスタンス自身を指す」ので、console.log(this.voice); の this は Bird クラスではなくそれぞれのインスタンスを指しています。

関数内の this が指すもの

関数は独立したオブジェクトであり、関数を定義したオブジェクトに所属するものではありません。 これを念頭に置いておくと、関数内の this が何を指すのかがイメージしやすくなります。

関数を変数に代入して呼び出す場合

次のコードは、obj1 で定義した fn 関数を obj1 から呼び出しているので、this は obj1 を見ています。これは簡単ですね。

var obj1 = {
    str: "obj1",
    fn: function() {            // obj1 で fn 関数を定義
        console.log(this.str);  // > "obj1"
    }
}
obj1.fn();  // obj1 から fn 関数を呼び出す

次のコードでは、obj2 の fn 変数に、obj1 で定義した fn 関数を代入後、obj2 から fn 関数を呼び出しています。 この場合、this は関数を定義した obj1 ではなく、呼び出した obj2 を見ています。

var obj1 = {
    str: "obj1",
    fn: function() {            // obj1 で fn 関数を定義
        console.log(this.str);  // > "obj2"
    }
}
var obj2 = {
    str: "obj2",
    fn: obj1.fn  // obj2.fn に obj1.fn の関数を代入
}
obj2.fn();       // obj2 から obj1 で定義した fn 関数を呼び出す

function() { console.log(this.str); } という関数オブジェクトは obj1 に所属しているわけではなく、独立したオブジェクトとして存在しています。

そもそも、「obj1 で fn 関数を定義」というのは、関数オブジェクトのアドレス情報を obj1.fn に代入しているだけです。 つまり、「obj2.fn に obj1.fn の関数を代入」というのも、obj1.fn に代入されている関数オブジェクトのアドレス情報を obj2.fn に代入しているだけのことです。 obj1.fn に関数が所属しているように見えるけれど、実際は obj1.fn も obj2.fn も、単に独立した関数オブジェクトへのアドレス情報を持っているだけで、どちらも同じことなのです。

こう考えると、関数内の this が、定義されたオブジェクトとは関係なしに、呼び出されたオブジェクトを指すというのも納得できます。

グローバルオブジェクトから呼び出す場合

グローバルスコープの範囲から呼び出された this は、グローバルオブジェクトを指します。

console.log の引数の str に this をつけない場合は、内側の関数の変数オブジェクトから順に名前解決をするため、ローカル変数 str を見にいきます。 (この挙動については、JavaScript のスコープチェーンとクロージャを理解する を参照。)

var str = "global";
function fn() {
    var str = "local";
    console.log(str);  // > "local" (自身の関数内のローカル変数を参照)
}
fn();

this をつけた場合は、this が呼び出し元であるグローバルオブジェクト(window)を指すので、グローバル変数の str を見にいきます。 this.strwindow.str に置き換わるようなイメージです。

var str = "global";
function fn() {
    var str = "local";
    console.log(this.str);  // > "global" (this が window を指す)
}
fn();  // グローバルスコープの範囲から呼び出す

イベントハンドラで関数を呼び出す場合

ボタンをクリックしたら、メッセージを表示する showMsg 関数を実行するようにしてみます。

HTML は次のとおりです。

<input type="button" id="btn" name="btn" value="Click Me">

まずは、イベントなしの場合の挙動を見てみます。this はグローバルオブジェクトを指しているため、this.msg はグローバル変数の msg を参照します。

var msg = "Hello";
function showMsg() {
    alert(this.msg);  // > "Hello" (this は グローバルオブジェクトを指す)
}
showMsg();

次に、input 要素のクリックイベントによって showMsg を呼び出してみます。すると this.msg が undefined を返すようになってしまいました。

var msg = "Hello";
function showMsg() {
    alert(this.msg);  // > undefined
}
window.onload = function() {
    document.getElementById("btn").onclick = showMsg;
}

this が何を指しているか確認するために、alert(this); に変えてみたところ、object HTMLInputElement と表示されました。イベントを元に関数を呼び出した場合、関数内の this はイベントの発生元の要素を指します。

var msg = "Hello";
function showMsg() {
    alert(this);  // > [object HTMLInputElement] (this は input 要素を指す)
}
window.onload = function() {
    document.getElementById("btn").onclick = showMsg;
}

call メソッドと apply メソッド

関数の呼び出し方によって this の指すオブジェクトがこれだけ変わると混乱してしまいますが、JavaScript のプロトタイプには、call と apply というメソッドが用意されています。 call と apply は、this の指すオブジェクトを自由にコントロールできます。

call も apply もほぼ同じ機能ですが、関数に渡す引数の指定方法が違います。 call は引数を個別に指定し、apply は配列でまとめて指定します。

call も apply の話だけで 1 エントリできてしまいそうなので、ひとまず参考サイトを貼っておきます。

考察

メソッド内の this は、メソッド呼び出し時にどのオブジェクトのプロパティとして呼び出されたかによって、this が何を指すのかが変わります。

なぜそんなややこしい仕様になっているのでしょうか。

Java の場合、メソッドはクラスであらかじめ定義しておき、それをインスタンス化することで初めて使えるようになります。インスタンス化した後に変更することはできません。しかし、JavaScript は柔軟な言語のため、あるオブジェクトで定義した関数を、他のオブジェクトの変数に代入して呼び出したりするなど、自由に再利用ができます。

もし this が「関数(メソッド)を定義したオブジェクトを指す」という仕様だとしたら、メソッドをインスタンス間で共有できても、メソッドを呼び出したインスタンスではなく、関数を定義したオブジェクトのメンバ変数を見に行ってしまいます。this が呼び出し元のオブジェクトによって指すものが変わるからこそ、同じメソッドを再利用できる仕組みになっているのだと思います。たぶん。

まとめ

  • クラスをインスタンス化するときに引数で渡した値を、this を使ってインスタンス自身にセットすることにより、そのインスタンスのメンバ変数として値を代入できる。
  • 関数内の this は、呼び出し時にどのオブジェクトのプロパティとして呼び出されたかによって、this が何を指すのかが変わる。
  • イベントハンドラで関数を呼び出した場合の this は、イベントの発生源のオブジェクトを指す。

今回参考にしたサイト & 本 Thx♡