javascriptのループ中にあるイベントの変数の扱い方

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
メビウスの輪……っぽい感じ

この記事を三行にまとめると

恥じぬ! 媚びぬ! マリアンヌ!!
即時関数っていうのを使って解決してみたいと思います
カードを出した瞬間に発動するインスタント呪文みたいなもん
すみません。どうタイトルをつけたら適切かってのが分からなくてこんなタイトルになっちゃいましたが……何が言いたいかってーと、javascriptでfor文なんかを使って処理を書くとき、例えばクリックイベントがある場合に、そのイベントの中で変数を使おうとすると、思うような挙動になってくれないことがあるので、それを何とかしたいってことです。

うん、タイトルとほとんど言ってること変わんないですね^^;

まあ、とりあえずどういうことか、実際にコード書きながら見ていきましょう。



ループ中のイベント

例えば、こんなコードがあるとします。

var array = ['button1','button2','button3','button4'];
for(i = 0; i < array.length; i++) {
  $button = document.createElement('button');
  $button.innerHTML = array[i];
  $button.onclick = function() {
    alert(i);
    alert(array[i]);
  };
  document.body.appendChild($button);
}

ボタンを4つ作って、クリックしたらそれぞれのインデックス番号と自分の名前をアラートする、例えばbutton1をクリックすれば「0」「button1」、button2をクリックすれば「1」「button2」がアラートされるという、そんなシンプルなコードです。

実際にこのコードを書いて動かしてみましょう。

サンプルページ

さて、どうだったでしょうか。

どのボタンを押しても、同じ結果しかアラートされませんね。「4」と「undefined」の二つ。

javascriptの場合、こういう書き方をすると、正しくそれぞれのボタンに応じた結果が出力されるわけではないのです。



こうなる原因

何でこんなことが起こるかって言うと、えーと……僕もあまりちゃんと説明はできないんですが、クロージャってやつが関係してまます。

クロージャって何よって言われるとそれこそ僕も説明できないんですが、こちらの猿でも分かるクロージャ入門さんによれば、そもそも言葉で説明するのが難しい概念らしいんで、説明できなくても何一つ恥じることはないのさ、ハーッハッハ!!

恥じぬ! 媚びぬ! マリアンヌ!!



上記のような処理を行う場合、本来なら4つ分のクリックイベントを生成したいのですが、この書き方だと、ページを読み込んだ時点でループ処理が終わってしまい、変数iにはループが終わった状態の値が入ってしまうので、どこを押しても4が入った状態でイベントが行われてしまうのです。クロージャが一つ分しか作られない……ってことで良いのかな?

こんな説明で大丈夫かしら? よく分かんねーって人は、とりあえずこんな書き方しても正しく動かねーってことだけ分かればオーケーです。

まあ、ようはあれよ。マラソンでグラウンドを十周する際、こっちは一周ごとのタイムを知りたかったのに、誰もそれを記録つけてくれなくて、ゴールしたときの合計タイムしか分からなかった、みたいなことよ。

うん、違うね。



即時関数を使ったやり方

じゃあどうすれば良いのかってことなのですが、今回は即時関数っていうのを使って解決してみたいと思います。

即時関数ってのもやっぱりちゃんとは説明できないんですが……即時的な関数のことです。関数を定義した瞬間に発動するって感じですかね。マジックザギャザリングで言えば、カードを出した瞬間に発動するインスタント呪文みたいなもんでしょうか。

つまり、この即時関数を使うことによって、ループが終わった状態でクロージャが作られるわけではなく、それぞれの処理の中でクロージャを作ることができるとか、そういう感じです。

var array = ['button1','button2','button3','button4'];
for(i = 0; i < array.length; i++) {
  $button = document.createElement('button');
  $button.innerHTML = array[i];

  //即時関数を使って書き換える
  (function(j){
    $button.onclick = function() {
      alert(j);
      alert(array[j]);
    };
  }(i));

  document.body.appendChild($button);
}

即時関数は「(function(変数){}(変数))」みたいな書き方をします。もし変数を渡すものが何もなければ「(function(){}())」って書けば動きます。

こうすれば、それぞれのボタンを押したときに違う結果が出力されます。

さっきと同じページですけど、念の為。

サンプルページ






即時関数を使うことでしか解決できないかっていうとそんなことはなくて、別の関数として切り出すとかいくつかやり方はありますが、「今まで即時関数って使ったことなかったからちょっと使ってみようかな〜」って思った人は、試してみると今日の運勢が良くなるかもしれない。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください