私の場合、Javaから移行してきて、JavaScriptのコードを書いているので、どうしてもthisで戸惑うことがあります。
https://qiita.com/takeharu/items/9935ce476a17d6258e27がうまくまとめてあって、理解するのに助かったことがありますが、ただ、まただいぶ曖昧になってきてたので、再度自分自身が確認するためにメモします。内容的には上のリンクと重複しますが、アロー関数などの情報を加えたりして、膨らませてあります。
- ※
- ちなみにES2015で書いてあります。
上のリンクでも書いてあるようにthisが使われるコンテキストが4種類しかないわけじゃありませんが、今回の記事は同じ4種類を取り上げて、まあこれぐらいは覚えておこうという感じになっています。
- メソッド呼び出しコンテキスト
- 関数呼び出しコンテキスト
- コンストラクタ呼び出しコンテキスト
- apply(call)による関数呼び出しコンテキスト
メソッド呼び出し・関数コンテキストの2つ
オブジェクト(インスタンス、レシーバー).method(); // →メソッド呼び出し
メソッドと関数の違いは各言語の定義の問題であったり、関数プログラミングとか全然違う意味で使われたりするので混乱しますが、ここでは関数の呼び出しをドットを使って呼び出すものをメソッド呼び出し、単独で呼び出すものを関数呼び出しと呼びます。
1
2
3
obj.method(); // →メソッド呼び出し
func(); // →関数呼び出し
さて、そのメソッド呼び出しする際の、そのメソッド内でのthisについてです。次のスクリプトを見てください。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Something {
constructor(v) {
this.value = v;
}
method() {
console.log(this.value);
}
}
var instance = new Something("Good Morning!");
instance.method(); // →Good Morning!
これはわかりやすいと思います。instance.method()
のinstance
(レシーバ)はSomethingクラスのインスタンス(オブジェクト)なので、thisはそのインスタンスを指しています。
1
2
3
4
5
6
7
8
9
var blackbird = {
value: null,
method: function() {
console.log(this.value);
}
};
blackbird.value = 'Yesterday';
blackbird.method(); // →Yesterday
これもクラス宣言をしていませんが、考え方は全く同じです。blackbird.method()
のblackbird
はそのまんま、最初に定義したオブジェクトなので、thisはそのオブジェクトを指しています。
1
2
3
4
5
6
7
8
9
10
11
var Because = function(v) {
this.value = v;
};
Because.prototype.method = function() {
console.log(this.value);
};
var julia = new Because('Julia');
julia.method() // →Julia;
これも同じです(書き方が色々あると、混乱してしまいそうですが・汗)。繰り返しですが、julia
がレシーバーなので、method内のthisはそのオブジェクトを指しています。
今回の話題とはズレますが、JavaScriptは関数もオブジェクトであり、変数に代入できるし、オブジェクトのkey=>valueのvalueにもなるので、結局は上3つはほぼ同じと考えて(継承とかを無視すれば)良いのかな(?)。
さて、実はこれには落とし穴があります。
1つは、時々失敗しますが(私です)、やや簡単な方です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var blackbird = {
value: null,
method: function() {
console.log(this.value);
},
getMethod: function() {
return this.method;
}
};
blackbird.value = 'Yesterday';
blackbird.method(); // →Yesterday
var func = blackbird.getMethod();
func();
実行結果は
1
2
Yesterday
undefined
です。
今度はgetMethodで関数を拾って、実行する場合です。定義自体はblackbirdオブジェクト内で定義されていますが、これを拾ったら純粋に普通の関数になり、レシーバなしに実行すれば、thisはトップオブジェクトになっているわけです。
2つめは、次のようなコールバックを伴うような場合です。
1
2
3
4
5
6
7
8
9
10
11
12
var help = {
value: null,
method: function() {
setTimeout(function() {
console.log(this.value); // → undefined
}, 1);
}
};
help.value = 'Anna';
help.method();
これは2つ上のbecause
とmethod内のconsole.log
がsetTimeout
内に入っている以外は同じです。が、実際にはundefined
が出力されます。
理由等の詳細はhttp://d.hatena.ne.jp/vividcode/20110106/1294336737あたりを読むと書いてありますが、要するに次に取り扱うsetTimeout
が関数呼び出しになっている上に、それに渡しているcallback関数も(たぶん)実行するときは関数呼び出しになっているからです。
そして、関数呼び出しの場合thisはトップレベルのオブジェクトを指すことになります(らしい・汗)。
次のコードを実行すると、method
の中から実行しているmethod2
のみがAnnaと出力されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function func() {
console.log(this.value + '/ func');
}
var help = {
value: null,
method: function() {
setTimeout(function() {
console.log(this.value + '/ setTimeout');
}, 0);
func();
console.log((function() {
return this.value + '/ 即実行関数'
})());
this.method2();
},
method2: function() {
console.log(this.value + '/ method2');
}
};
help.value = 'Anna';
help.method();
もちろん、method2
自体の呼び出しがthis.method2()
というように、thisをレシーバにしているからです。
では、this.value
をしっかり出力するにはどうすれば良いでしょうか。方法は4つあると思います(まだあるかもですが・汗)。
- thisじゃなくthatとかselfを使う方法。
- bindでthisを束縛する方法
- apply(call)で関数を実行する際、thisを束縛する方法
- アロー関数を使う方法
1はthisを待避していて、callbackの中ではthisの代わりにthatを使う方法です(これは古くから行われているものです)。
1
2
3
4
5
6
7
8
9
10
11
12
var michelle = {
value: null,
method: function() {
var that = this;
setTimeout(function() {
console.log(that.value);
}, 0);
}
};
michelle.value = 'Birthday';
michelle.method();
2は関数自体をレシーバにして、bind(this)
する方法です。
1
2
3
4
5
6
7
8
9
10
11
var taxman = {
value: null,
method: function() {
setTimeout((function() {
console.log(this.value);
}).bind(this), 0);
}
};
taxman.value = 'Flying';
taxman.method();
3は2に似ていますが、thisをbindしてすぐ実行する感じです。逆にそれなので、callbackには使えません。
1
2
3
4
5
6
7
8
9
10
var piggies = {
value: null,
method: function() {
func.bind(this)(); // ↓と同じ
func.call(this);
}
};
piggies.value = 'Boys';
piggies.method();
そして、最後のアロー関数ですが、これだと定義されるのthisを束縛します(人によって言い方が違うのですが、束縛しないというのが正しいのかも。束縛しないから、thisがそのまま関数の外側のthisになるという解釈が正しい気がします)。
1
2
3
4
5
6
7
8
9
10
11
var money = {
value: null,
method: function() {
setTimeout(() => {
console.log(this.value);
}, 0);
}
};
money.value = 'Chains';
money.method();
つまり、bindやらcallなどを利用しなくてOKということになります。
ただ、やはり危険も存在ます。functionを書かなくて良かったりで、ついあまり考えずに使うと失敗します(もちろん、それは私ですw)。
1
2
3
4
5
6
7
8
9
10
11
var wait = {
value: null,
method: () => { // ①
setTimeout(() => {
console.log(this.value); // ②
}, 0);
}
};
wait.value = 'Matchbox';
wait.method();
これはmethodの定義でも、setTimeoutのコールバックにもアロー関数を使っていますが、これだとundefined
が出力されてします。
①の時点で、外側のthisは、nodeだったらトップモジュール(ブラウザだったらwindow)になっており、②の時はそれを束縛せずそのまま利用するのでやはりトップモジュールになっているわけです。
次の例を見てください(↑のを少しだけ書き換えたもの)。
1
2
3
4
5
6
7
8
9
10
11
12
13
var wait = {
value: null,
method: () => {
console.log(this.value + ' in method');
setTimeout(() => {
console.log(this.value + ' in callback');
}, 0);
},
that: this
};
wait.value = 'Matchbox';
wait.method();
console.log(wait.that);
これは
1
2
3
undefined in method
{}
undefined in callback
と出力されますが、重要なことはwaitオブジェクトの中のthatがさしているthisが{}で出力されていて、自分自信が出力されていないことです。
これだともっとわかるかもしれません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.value = 'Beatles';
var wait = {
value: null,
method: () => {
console.log(this.value + ' in method');
setTimeout(() => {
console.log(this.value + ' in callback');
}, 0);
},
that: this
};
wait.value = 'Matchbox';
wait.method();
console.log(wait.that);
外側でthis.value = ‘Beatles’とやっておくと、しっかり出力しています。
1
2
3
Beatles in method
{ value: 'Beatles' }
Beatles in callback
つまり、
という感じで、ずっと束縛されずトップのオブジェクトが引き継がれてきています。
そして、最後に、これはどうなるでしょう?
1
2
3
4
5
6
7
8
9
10
11
12
function paul (name) {
this.value = name;
this.method = () => {
console.log(this.value + ' in method');
setTimeout(() => {
console.log(this.value + ' in callback');
}, 0);
};
}
var george = new paul('Ringo');
george.method();
は
1
2
3
Ringo in method
Beatles in callback
Ringo in callback
となります。やはりcallbackや関数リテラルには注意が必要そうです。