先日(といってもすでに半年前…)、HiroshimaRB にて「思ったようにコードが動かない!」という題目で LT をしたのですが、JavaScript のクロージャーのところで質問がありました。私にもっと理解力があって、冷静に対応できればよかったのですが、ぐでぐでな対応になってしまったので、反省を込めてここに記す(汗
- 問題のコード
サンプルコードはこのようなものでした。
<html><body> <span id="value" style="font-size: 48pt;" ></span> <script type="text/javascript"> for (i=0; i<5; i++) { f = function() { // i の値を追記する document.getElementById("value").innerHTML += i + "<br>"; } // i 秒後に i を追加表示する(予定) setTimeout("f()", i*1000); } </script> </body></html>
期待する結果は、1 秒ごとに 0 → 1 → 2 → 3 → 4 と表示される予定です。
0
1
2
3
4しかし実行結果は、 0 → 1 → 2 → 3 → 4、とはならず、すべて 5 で表示されます。
5
5
5
5
5 - 指摘の内容
この時、for のところに var を入れればよいのでは、という指摘がありました。
指摘の理由は、上記のコードの変数 i スコープがグローバルです。だから値を共有してしまったのではないか、ということでした。
つまり、上記のコードの以下の部分を、for (i=0; i<5; i++) {
以下のように「var」を追加し、変数 i のスコープをグローバルからローカルに変えれぱ解決するのではないか?という指摘です。
for (var i=0; i<5; i++) {
しかし、これでは結果は変わらず、期待した動作にはなりませんでした。
- グローバル変数とローカル変数
ところで、JavaScript では、なにも宣言せずに使用を始めた変数というのはグローバル変数相当(Window(=this) のメンバー) になります。(*1)
試してみます。<html> <body> <form> <script> function click1() { x = "aaa"; alert(x); } function click2() { alert(x); alert(this.x); alert(window.x); } </script> <input type="button" value="ボタン1" onClick="click1()"> <input type="button" value="ボタン2" onClick="click2()"> </form> </body> </html>
ボタン1 を押すと、"aaa" で alert が表示されます。
その後、ボタン2 を押すと、click2() では x は宣言や代入をしていないはずなのに "aaa" で alert が表示されます。
つまり click1() で代入した x のスコープはローカル変数ではなく、グローバル変数に相当することがわかります。x をローカル変数にするためには、代入文の前に var をつけます。(*2)
こうすると、click1() を実行した後に click2() を実行しても、click2() の x は定義されていないため、alert は表示されません。<html> <body> <form> <script> function click1() { var x = "aaa"; alert(x); } function click2() { alert(x); alert(this.x); alert(window.x); } </script> <input type="button" value="ボタン1" onClick="click1()"> <input type="button" value="ボタン2" onClick="click2()"> </form> </body> </html>
- タイマー要素を排して、もう一度眺めてみる
指摘の内容は、
- グローバル変数なので、
- 各 function がグローバル変数を参照(共有)してしまい、
同じ値(x の値が for を抜けた時の値)になってしまうのではないか?という話でした。この指摘は、半分あたりだとおもいます。
しかし単純にローカル変数にしてもうまくいかないことを試してみます。
上記コードではタイマー(SetTimeout)に渡すようになっていますが、事を単純にするため、タイマーは使わないコードに変更します。<html> <body> <form> <script> function click1() { var f = new Array(5); // 仕込 for (var i=0; i<5; i++) { f[i] = function() { alert(i); } } // 実行 f[0](); f[1](); f[2](); f[3](); f[4](); } </script> <input type="button" value="ボタン1" onClick="click1()"> </form> </body> </html>
結果は、5 回とも "5" で alert が表示されます。
- 原因
上記コードの「仕込み」の部分の「alert(i)」の部分に注目します。
この alert(i) は for 文のループ中では実行されません。function の宣言(定義)をしているだけなんですね。
実行はその下の「実行」のところの f[0]()~f[4]() で行っています。alert(i) の i の値がどこで確定(*3)するかというと、function f[i]() の宣言時ではなく、f[0](); の実行時です。
実行は for を抜けた後です。forを 抜けた後ということは、当然 i = 5 ですから、alert もすべて 5 で表示されます。 - 対策
f[i] の中の i は、値ではなく変数 i を参照しています。これが原因で、f[i] は実行したときの変数 i の値を表示してしてしまいます。
ということは、変数 i を参照するのではなく、for を実行したときの変数 i の"値"を何かしらの方法で保存してやればよいことになります。手順は以下の通り。
1.f[i]() を作る処理を g() で囲みます。
2.g() の中で、ループ変数 i を g() のローカル変数 x に代入します。
3.for ループの中で、g() を実行します。こうすると、function を使わず直截変数 i を参照しているケースと違って、以下のように動作します。
- g() は for ループ内で実行するので、var x = i により、変数 x に変数 i の値が代入されます。
- g() は終了するとき、x は i の値で固定されます。
- for の 2 ループ目に g(); を実行するときは、変数 x のインスタンスは 1 ループ目のものとは違うものが与えられます。
#変数 x は g() のスコープにいるので、前回ループで実行した g() での値は引き継がない。 - したがって、f[i] で参照している x は ループ中の i と同じ値になります。(*4)
つまり、変数 i のスコープは click2() ですが、変数 x のスコープは g() です。したがって変数 x は f[i] では共有されず、それぞれ別々の値(=インスタンス)を保持することになります。
<html> <body> <form> <script> function click2() { var f = new Array(5); for (var i=0; i<5; i++) { g = function() { /* click2()のスコープである i を g() の スコープである x に代入する */ var x = i; f[i] = function() { alert(x); } } g(); // ここで g() の中の x が確定する。 } f[0](); f[1](); f[2](); f[3](); f[4](); } </script> <input type="button" value="ボタン1" onClick="click2()"> </form> </body> </html>
(*1)
Perl もそうみたいです。(ただ、perl は var ではなく my と書くようです)
VB(VBS や VBA も)は、ルーチン内で代入したものはデフォルトではローカルスコープ。但し、あとから同名のグローバル変数を作るとグローバル化してしまう。
JavaScript のスコープは結構変態です。(と思う)(*2)
実は var は後に記述してもいいらしいです。
宣言を後に回した記述例。こう書いても x はローカル変数として認識します。
この点も変態ですね。function click1() { x = "aaa"; alert(x); var x }
(*3)
正確には i の値を「確定」しているわけではなく、ただ単純に i の値が参照されているだけですが…(*4)
function が return すればローカル変数も解放されるはずですが、この場合はされません。
ローカル変数のキャプチャという機能によって、そのローカル変数を参照している function がいなくなるまで保持されています。