jiichan.com

PROGRAMMING

javascript
CSS
PHP
Java

forループのカウンタを使用している関数の定義

forループ内でカウンタ変数を利用し関数を複数定義する場合があります。
その際によく勘違いするのがカウンタ変数の値です。
スコープチェーン形成のタイミングを考えれば、問題なくできそうです。

意図どおりに結果が出ない例

次の例は画面に0から4までの数字を連続で表示しようとしたコードです。

var func = [];
for (var i = 0; i < 5; i++) {
	func.push(function(){return i}); 
}
var result = document.getElementById("result");
var str = "";
for (var j = 0; j < func.length; j++) {
	str += func[j]() + "&nbsp;";
};
result.innerHTML = str;	// 「5 5 5 5 5」と表示される

この場合、画面には「5 5 5 5 5」と出力されます。
3行目の関数の定義部分がポイントで、ここでは「変数iを参照するという関数定義」が5回行われています。 これはあくまでも変数への参照で、0、1、2、3、4の値では無いということです。
iの値は変化していますが、関数そのものはiを参照するという同じ定義を繰り返しています。

func.push(function(){return i}); 

次が、8行目の関数の実行部です。

str += func[j]() + "&nbsp;";

この時点での変数iの値は、ループを止めた5になっています。したがって、それぞれの関数はすべて参照値である5を出力することになります。
ここでは、関数を実行する度に、それぞれの関数ごとにActivation Objectiが生成されスコープチェーンが形成されるということが行われています。 それぞれのActivation Objectのプロパティiの値にはすべて5がセットされています。

意図通りにするためには

それでは、意図通りに出力するためにはどうした良いのでしょうか。

最初のループは 1. 変数iの変化 2. 関数の定義(変数iへの参照) 1と2の繰り返しでスコープチェーンが形成されていないため、この時点では変数iの値が見えません。

次のループでは 1. 変数iは最初のループ終了で5になっている 2. 関数が実行される 3. 関数の実行でスコープチェーンが形成される 4. 変数iが見えるようになったが値が5になっているので、値5を返す関数が出来上がる 1から4の繰り返しになり、すべての関数は5を返すことになります。

そこで、変数iの値が変化する度に関数を実行してしまえば、その都度スコープチェーンが形成されて、意図通りに出力されるのではないでしょうか。 3行目を即時関数に変えてみました。

func.push(
	(function(n){
		return function(){return n;};
	})(i)
);

定義と実行を同時に行いカウンタ変数iを引数にした関数です。
最初のループで 1. 変数iの変化 2. 関数の定義(変数iへの参照) 3. 関数の実行 4. スコープチェーンが形成される 5. 変数iの値が見えるので、それぞれの値(0~4)を返す関数の出来上がり それぞれの値を返す関数が1サイクルごとに出来上がっています。

次のループでは 1. 各値を返す関数を実行する その結果、意図通りに「0 1 2 3 4」と出力することができました。

スコープチェーンが形成されるタイミングをよく考えるということがわかりました。

カウンタ変数を再度使用したら

最初の例(うまくいかなかった例)では、関数実行部分のループでカウンタ変数をjとしていました。
試しにこの部分も最初のループを同様にiを使用してみました。

var func = [];
for (var i = 0; i < 5; i++) {
	func.push(function(){return i;}); 
}
var result = document.getElementById("result");
var str = "";
for (var i = 0; i < func.length; i++) {
	str += func[i]() + "&nbsp;";
};
result.innerHTML = str;	// 「0 1 2 3 4」と表示される

変数iを参照しているので、変数iが初期化されたことで結果的に即時関数の例と同じになります。