クロージャとレキシカルスコープ
自分なりのまとめです。
【他、参考となるサイト】 http://d.hatena.ne.jp/keyword/%a5%af%a5%ed%a1%bc%a5%b8%a5%e3 http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_03.html http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_04.html
内部関数
関数内で更に関数を定義することができます。
function test(test_str){ function sub(sub_str){ alert(sub_str); } sub("TEST:" + test_str); } test("!!!"); //「TEST:!!!」と表示される。 //←ここでsub("!!!");とはできない。
関数subをこのように定義すれば、親関数test内でしか使わないことがはっきりしますし、名前の競合も防ぎやすくなります。
クロージャ
内部関数とクロージャはほぼ同義なんですが、ここでは分けてみました。親関数の外に参照が渡される内部関数をクロージャと呼ぶことにします。→すみません、以降、内部関数もすべてクロージャと記述させてください。
「親関数の外に参照が渡される」とは、具体的には
- 返却値
- 関数呼び出しの引数
- (グローバル等の)親スコープ変数への格納
- 既存オブジェクトのメンバとして登録*1
等になります。
var g_sub; function other(a_sub){ a_sub("ARG"); //「TEST:ARG」と表示される。 } function test(){ function sub(sub_str){ alert("TEST:" + sub_str); } other(sub); //引数 g_sub = sub; //グローバル変数へ格納 return sub; //返却 } var r_sub = test(); r_sub("RET"); //「TEST:RET」と表示される。 g_sub("GLB"); //「TEST:GLB」と表示される。
注目してほしいのは、関数testの実行終了後も、関数subが有効であることです。
レキシカルスコープ
親関数の変数・仮引数を使用可能
クロージャは、親関数の変数・仮引数を使用することができます。
var g_sub; function other(a_sub){ a_sub("ARG"); //「TEST:0:ARG」と表示される。 } function test(test_str){ var test_count = 0; function sub(sub_str){ alert(test_str + ":" + test_count + ":" + sub_str); } other(sub); //引数 g_sub = sub; //グローバル変数へ格納 return sub; //返却 } var r_sub = test("TEST"); r_sub("RET"); //「TEST:0:RET」と表示される。 g_sub("GLB"); //「TEST:0:GLB」と表示される。
ここで注目してほしいのは、関数testの実行終了後も、その変数・仮引数が有効であるということです。このスコープ特性をレキシカルスコープといいます。外に渡されたクロージャへの参照が保持される間、その親関数の変数・仮引数も保持されます。
親関数の変数・仮引数は、その実行毎に保持
上記の例で、関数subにtest_countをカウントアップする処理を追加してみます。
var g_sub; function other(a_sub){ a_sub("ARG"); //「TEST:1:ARG」と表示される。 } function test(test_str){ var test_count = 0; function sub(sub_str){ test_count++; //カウントアップ alert(test_str + ":" + test_count + ":" + sub_str); } other(sub); //引数 g_sub = sub; //グローバル変数へ格納 return sub; //返却 } var r_sub = test("TEST"); r_sub("RET"); //「TEST:2:RET」と表示される。 g_sub("GLB"); //「TEST:3:GLB」と表示される。
カウントアップされるのが分かると思います。つまり、クロージャ間で親関数の変数・仮引数は共有されているのです。
ここで、test関数を2回実行してみます。
var g_sub; function other(a_sub){ a_sub("ARG"); } function test(test_str){ var test_count = 0; function sub(sub_str){ test_count++; //カウントアップ alert(test_str + ":" + test_count + ":" + sub_str); } other(sub); //引数 g_sub = sub; //グローバル変数へ格納 return sub; //返却 } var r_sub = test("TEST"); r_sub("RET"); g_sub("GLB"); //もう1回 var r_sub_m = test("TEST_M"); r_sub_m("RET"); g_sub("GLB"); //g_subを3回実行 g_sub("GLB"); g_sub("GLB"); //初回に返却されたクロージャ r_sub("RET");
実行結果は以下です。
以下が順番に表示される。 TEST:1:ARG TEST:2:RET TEST:3:GLB TEST_M:1:ARG TEST_M:2:RET TEST_M:3:GLB TEST_M:4:GLB TEST_M:5:GLB TEST:4:RET
実行一回目のクロージャと、実行二回目のクロージャで変数が共有されていないことが分かります。つまり、親関数の実行毎に変数・仮引数が保持されているということです。
クロージャはグローバル(windowオブジェクトのメンバ)として実行される
クロージャをそのまま実行すると、グローバル(windowオブジェクトのメンバ)として実行されます。このため、親関数内のthisとクロージャ内のthisは意味が異なります。
<button id="button">TEST</button> <script> var button = document.getElementById("button"); function test(){ alert(this.id); //「button」と表示される。 alert((this == button)); //「true」と表示される。 function sub(){ alert(this.id); //「undefined」と表示される。 alert((this == window)); //「true」と表示される。 } sub(); } button.onclick = test; </script>
要素のonclickプロパティに登録された関数は、イベント発生時、要素のメンバとして実行されます。(thisが要素を意味する。) クロージャで親関数のthisを参照したい場合は、以下のように親関数内でthisを変数に格納して参照します。
<button id="button">TEST</button> <script> var button = document.getElementById("button"); function test(){ alert(this.id); //「button」と表示される。 alert((this == button)); //「true」と表示される。 var _this = this; //変数にthisを格納。 function sub(){ alert(_this.id); //「button」と表示される。 alert((_this == window)); //「false」と表示される。 } sub(); } button.onclick = test; </script>
または、クロージャをapply/callメソッドを使用して呼び出すことでも可能です。
<button id="button">TEST</button> <script> var button = document.getElementById("button"); function test(){ alert(this.id); //「button」と表示される。 alert((this == button)); //「true」と表示される。 function sub(){ alert(this.id); //「button」と表示される。 alert((this == window)); //「false」と表示される。 } sub.apply(this); } button.onclick = test; </script>
呼び出しにオブジェクトを必要としない分、前者のほうが汎用性は高いと思います。
クロージャは他のコードより優先して生成される(※例外あり、後述)
問題です。以下のalertは何が表示されるでしょう。
function test(){ var count = 0; count++; function sub(){ alert(count); //??? } count++; return sub; } var retFunc = test(); retFunc();
親関数の変数、およびそれを使った処理が、クロージャの前にあるか後ろにあるかは関係ありません。問題はクロージャ実行時にどうなっているかです。上記例の場合は、return後の状態になります。なので、答えは「2」です。
・・・というか、クロージャの親関数内での位置には意味がありません。コメントいただいて気が付いたのですが
JSではfunctionやvarによる宣言部分(変数の初期化代入は宣言に含まれません)は、スコープ内で他のコードよりも先に処理される
のですね。
function test(){ var str = "TEST"; return sub; function sub(){ alert(str); } } var retFunc = test(); retFunc(); //「TEST」と表示される
例外:クロージャ(関数)が式に組み込まれている場合
ただし、クロージャが式に組み込まれている場合は少し話が違います。以下の実行結果は?
function test(){ var str = "TEST"; return sub; var wrap = function sub(){ alert(str); }; } var retFunc = test(); retFunc();
正解は、「ブラウザによって異なる」です・・・。
- IE:「TEST」と表示される。
- Firefox:3行目でエラー。(sub is not defined)
- Opera:3行目でエラー。(Reference to undefined variable: sub)
あとで気が付いたのですが、FirefoxやOperaでは、subが有効なスコープはその式関数の中だけになるようです。IEについては、あらかじめ生成した上、実行時にも生成しているような気がします。
例1
過去に回答した質問question:1180098481を題材にさせていただきます。
問題です。次のコードの最終2行実行時、何が表示されるでしょう。
var a = new Array(); function f(){ for(var i = 0;i < 10;i++){ a[i] = function(){ window.alert(i); }; } } f(); a[0](); //??? a[9](); //???
「0」と「9」、と思えるかもしれません。実際、質問者の方もそう思われたのです。しかし、すでに挙げたクロージャの性質、
- クロージャの親関数の変数・仮引数は、その実行毎に保持される
という点や、今までの例を踏まえれば、2回とも「10」が表示される、ということがご理解いただけると思います。
ここでさらに問題。配列aに格納されているクロージャは、それぞれ同じものでしょうか、それとも別のものでしょうか。
var a = new Array(); function f(){ for(var i = 0;i < 10;i++){ a[i] = function(){ window.alert(i); }; } } f(); alert((a[0] == a[9])); // true or false ?
正解はfalseです。別なんですね。前述したように、式に組み込まれているクロージャは、その実行時に生成されているのです。これはIEでもFirefox、Operaでも同じです。ただ、使用している変数が同じなので、同じ挙動になるのです。
さて、それぞれ違う値を表示する関数を配列aに登録するにはどうしたらよいのでしょう。
var a = new Array(); function f(){ function make(j){ return function(){ window.alert(j); }; } for(var i = 0; i < 10; i++){ a[i] = make(i); } } f(); a[0](); a[9]();
上記は私の回答です。ちょっとややこしいですね^^; クロージャを返すクロージャmakeを作成して、for文で実行しています。こうすることで、make実行毎に仮引数jが保持されるので、配列aの関数は、それぞれ違う挙動になるのです。
例2
クロージャの威力がよく発揮されるシーンとして、タイマーイベントや要素のイベントへの関数登録があります。
<html> <head> <script> function test(id){ var elem = document.getElementById(id); var count = 0; elem.onclick = start; var tid = null; function start(){ tid = setInterval(countup, 1000); elem.onclick = stop; } function countup(){ elem.innerHTML = count; count++; } function stop(){ clearInterval(tid); elem.onclick = start; } } window.onload = function(){ test("div1"); test("div2"); } </script> </head> <body> それぞれクリックするとスタート←→ストップが切り替わります。 <div id="div1" style="border:solid blue 2px;background-color:#ccccff">クリックするとスタート</div> <div id="div2" style="border:solid red 2px;background-color:#ffcccc">クリックするとスタート</div> </body> </html>
青い枠のDIVと赤い枠のDIVがあります。それぞれクリックすると、1秒ごとに数値をカウントアップして表示します。もう一度クリックすると停止し、更にクリックすると再開します。それぞれの数値は独立しています。
この機能を、関数testのみで実装しています。タイマー識別子やカウンタをグローバルエリアに持つ必要はありません。また、表示要素が増えたとしても、その分関数testを呼び出せばいいだけです。関数testに改変は必要ありません。こんなことができるのも、レキシカルスコープの特性があるからです。
おわりに
レキシカルスコープは、「prototype.js解読」のときにEnumerableクラスのeachメソッドで出くわして、散々悩みました^^; 経験言語(CやJava)にはない話だったので・・・。でも、いちど理解できると、JavaScriptが非常に効率よく組めるようになり、楽しくなった覚えがあります。この記事が、みなさんのJavaScript効率化の一助になれば幸いです。
・・・また、間違いがありましたらコメント等でご指摘くださいますよう、お願いいたします。。。m_ _m