javascriptにおけるスコープとクロージャについて
クロージャとかスコープの理解が自分の中であいまいなので整理しておく。
スコープについて
- javascriptにおけるスコープはfunction単位
- ブロックレベルのスコープは無い
- 変数はそれが作成されたスコープのプロパティ
- webブラウザ内で実行される場合、グローバルスコープはwindowオブジェクト。なので、グローバル変数は実はwindowオブジェクトのプロパティになっている。
var hoge = 'hage'; // hoge == window.hoge
- functionスコープにおいて var 無しで 変数に代入を行った場合は、グローバルなスコープにプロパティが追加される。(webブラウザで実行されている状況ではwindowオブジェクトのプロパティになる)
function test() { hoge = 'hage'; } // hoge == window.hoge
クロージャについて
javascriptにおけるクロージャについての理解している内容をまとめておく。
- クロージャはそれが作られたスコープの各種参照(プロパティ, メソッド)を抱え込むことができる。
- ただし、それはあくまでも「参照」であることに注意する必要がある。
- よくははまるのがループでクロージャ作る場合。javascriptではブロックレベルでのスコープが無いという特性を忘れがち。
ハマる例:
forループで1秒後から10秒後まで1秒毎に「x秒後」というようなalertを上げるようなクロージャを登録する
(うまくいなない例)
for(var i = 1; i <= 10; i++ ) { setTimeout(function() { alert(i + "秒後"); }, 1000 * i); }
→全部"11秒後"と表示される
理由は、ループで生成されう全てのクロージャが同一のスコープで作成されているため。setTimeout()に渡される無名関数は変数 iへの参照を抱え込んでいるが、全てのクロージャは同じオブジェクト"i"を参照している。forループの実行によって i の値は最終的に 11 に変更される。 よって全てのクロージャから見た i の値は 11 となってしまう。
(解決策)
forループの実行毎に異なるスコープを作ってあげればよい。
for(var i = 1; i <= 10; i++ ) { (function(){ var num = i; setTimeout(function() { alert(num + "秒後"); }, 1000 * i); })(); }
→ちゃんと "1秒後", "2秒後",... "10秒後"と表示される。
ポイントは for ループの中の (function(){ ... })(); の部分。ここで、ループの実行毎にスコープを作って、そのスコープの変数である num に i の値をコピーしている。それにより、それぞれのループの実行時点の i の値を保持することができる。
(function(){})() がややこしくて気に入らない場合は、その部分を関数呼び出しに置き換えてもよい。要は、その繰り返し処理の「実行時点に限定されたスコープ」が用意できれば良い。
たとえば...
for(var i = 1; i <= 10; i++ ) { registerTimeout(i); } function registerTimeout(num) { setTimeout(function() { alert(num + "秒後"); }, 1000 * i); }
でもうまくいく。forループの変数 i は、registerTimeout()関数の呼び出し毎に作成されるスコープでnum変数に格納され、 setTimeoutの引数となっている無名関数からは、それぞれの無名関数が作成された時点のnumが参照されるので...
って、書いてて自分でわからなくなったので、あとで図で説明してみる。
※疑問:上記ではうまくかない場合があるはず。参照しようとしている対象がオブジェクトの参照である場合で、そのオブジェクトの内容がループによって変化するものである場合はうまくいかないはず??
上記の疑問・説明図についてはまた後日...
(つづく)