皆さん、こんにちはベル子です。
このブログでコード貼り付けると見た目が悪くて耐えられないので
これからgistを使うことにしました。
デキるレディーはいつもオシャレ。
ということで本題です。
こないだのES2015でうまく説明できなかったfor文で同じ値が参照され続けてしまう罠のサンプルを作成したので、解説しようと思います。
まずは下のコードを見て出力結果を予想してみてください。何が表示されるでしょうか?
これを書いた人が期待しているのは、以下のような出力結果です。
0 hello I'm hanako
1 hello I'm hanako
2 hello I'm hanako
ですが、実際は以下のように出力されます。
※以下はJS Binでの出力結果です。
3 hello I'm JS Bin Output
3 hello I'm JS Bin Output
3 hello I'm JS Bin Output
上のコードには2つの問題点があり、それの原因となっているのがjavascriptのスコープです。
スコープというのは「変数が参照できる範囲」のことです。
元々、ES6以前のjavascriptにはブロックスコープがありませんでした。
ブロックスコープというのはif文やfor文の{}で囲まれた範囲内でしか変数を参照できなくさせるスコープのことです。
なので、for文の初期化で定義したカウンタ変数iはfor文の外で定義されたのと、スコープ上は変わりがありません。
var i = 0;
for (i = 0; i < 3; i++) {
setTimeout(function(){
console.log(i + " hello I'm " + this.name);
}, 0);
そしてsetTimeoutの中のコールバック関数には、iという変数の参照を渡しているだけなので、ループが先に実行されて、ループが終わったあとでsetTimeoutの中の関数が実際に実行される際には最後に代入された3を参照してしまうので、期待の結果が得られません。
これをvar i = 0の部分をlet i = 0に置き換えてみると、ループの度に新しいiが定義されて、その新しいiを関数に渡すことになるので、期待どおりにインデックスを出力してくれます。
そしてもう一つはthisの参照先の問題です。
setTimeoutの中のコールバック関数は、setTimeout() が呼び出された関数とは「別の実行コンテキスト内で」実行されてしまうので、結果的にグローバルオブジェクトを参照することになります。
実行コンテキストってなんだよって思うと思うんですが、javascriptは関数を実行するたびに新しい実行コンテキストというものを作成しています。
関数の外にあるコードはどうなるかというとグローバル実行コンテキストというのがあって、グローバルオブジェクトで管理されています。
要するに、setTimeoutのコールバック関数内にthisを書いてしまうと、this=グローバルオブジェクトになるということです。
だから、期待の値が出力されません。
これをアロー関数を使うと、setTimeoutを囲ってる関数の実行コンテキストのthisの値が設定されるようになります。
難しい言い方をしましたがsetTimeoutを囲ってる関数のthisとコールバック内のthisが同じになるということです。
というわけで、ES2015のブロックスコープletとアロー関数を使って書き換えると、
すごく直感的に分かりやすいコードになるというわけです。めでたしめでたし。