次はJavaScriptでのクロージャについてです。よく間違えやすいケースについて書いてみます。
まずは、シナリオを考えます。
- ボタンが3つある
- 1番目のボタンをクリックしたら1をアラートで表示
- 2番目のボタンをクリックしたら2をアラートで表示
- 3番目のボタンをクリックしたら3をアラートで表示
これをコードにしてみましょう。まずはHTMLだけ。
<html>
<head>
<script type="text/javascript">
</script>
</head>
<body>
<form name="foo">
<input type="button" id="button1" value="1">
<input type="button" id="button2" value="2">
<input type="button" id="button3" value="3">
</form>
</body>
次にScriptタグの中身を書いて見ましょう。
function button1Click(){
alert(1);
}
function button2Click(){
alert(2);
}
function button3Click(){
alert(3);
}
//前提として、bodyタグにonload="windowOnload()"が記述済みとする
function windowOnload(){
var button1 = document.getElementById("button1");
button1.onclick = button1Click;
var button2 = document.getElementById("button2");
button2.onclick = button2Click;
var button3 = document.getElementById("button3");
button3.onclick = button3Click;
}
これは取り合えず非常に冗長ですが、オーソドックスな書き方です。ではこれをクロージャで書いてみましょう。
window.onload = function(){
document.getElementById("button1").onclick = function(){
alert(1);
};
document.getElementById("button2").onclick = function(){
alert(2);
};
document.getElementById("button3").onclick = function(){
alert(3);
};
};
少しシンプルになりました。色々なライブラリとのコンフリクションがあったりするので、最近のJavaScriptの主流の書き方では、極力グローバルの変数や関数を使いません。
ただこれでもまだ冗長なので、普通に以下のようなコードを書かれると思います。
window.onload = function(){
for(var i = 1; i <= 3; i++){
var button = document.getElementById("button" + i);
button.onclick = function(){
alert(i);
};
}
};
これを実行すると分かると思いますが、どのボタンをクリックしても4が表示されます。ループの中のクロージャはループカウンタの変数iに対する参照を持ちますが、最後に評価された結果の4が全てのクロージャから参照されてしまうのです。
では、上記コードに手を入れて正しく動くようにしてみます。
window.onload = function(){
for(var i = 1; i <= 3; i++){
var button = document.getElementById("button" + i);
//もう一つ関数で囲む
(function(n){
button.onclick = function(){
alert(n);
};
//iを渡す
})(i);
}
};
匿名関数にiを都度渡していますが、これによってiを参照しなくなります。関数に渡された仮引数を都度参照するので、この場合正しく動きます。
では最後に効率化を行いましょう。関数リテラルはキャッシュされず、インタプリタによってそのステートメントが評価されるたびに新しい関数オブジェクトが作られます。すなわち"(function(n){"で始まる関数のオブジェクトは都度つくられるのです。
window.onload = function(){
var click = function(n, button){
button.onclick = function(){
alert(n);
};
};
for(var i = 1; i <= 3; i++){
var button = document.getElementById("button" + i);
click(i, button);
}
};
関数は都度作られるというのは、以下のコードで確認できます。
var list = [];
for(var i = 0; i < 3; i++){
list.push(function(){});
}
alert(list[0] == list[1]);
alert(list[1] == list[2]);
alert(list[0] == list[2]);
上記は全てfalseになります。