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が参照されるので...

って、書いてて自分でわからなくなったので、あとで図で説明してみる。

※疑問:上記ではうまくかない場合があるはず。参照しようとしている対象がオブジェクトの参照である場合で、そのオブジェクトの内容がループによって変化するものである場合はうまくいかないはず??

上記の疑問・説明図についてはまた後日...

(つづく)