クロージャの理解がテキトーだと引っかかる罠

まあ当然のように皆が通る道(→イベントに渡される関数オブジェクトをインスタンスのメソッドだと誤解してしまうミス)もハマったわけですが、それは克服した上でさらにはまったのが以下のような罠。

ボタンが5つあって(idはbutton_1〜button_5)、それぞれにほぼ同じイベントハンドラを設定したいとする。労力を軽減するためにforループで回してonclickに設定するとしようか。まず素直に下のように書いてみた。

<script type="text/javascript">
function init() {
  for(var i=0; i<5; i++) {
    var button = document.getElementById('button_'+(i+1));
    button.onclick = function() {
       alert('私は' + (i+1) + '番目のボタンです');
    }
  }
}
</script>
<body onload="init()">
  <button id="button_1">1</button>
  <button id="button_2">2</button>
  <button id="button_3">3</button>
  <button id="button_4">4</button>
  <button id="button_5">5</button>
</body>

CやJava等の言語に慣れていると、一見上のコードで良さそうに思えるがダメなのだった。JavaScriptでは関数は実行時に評価されるので、ボタン1〜5には確かに指定した関数がセットされるがボタンが押されたタイミングではi+1=6なのである。つまりどのボタンを押しても「私は6番目のボタンです」となり意図した結果にならない。

さて20日ほど前の私は、アサハカにも「クロージャで包めばいいんじゃね?」と考え以下のように書いた。

<script type="text/javascript">
function init() {
  for(var i=0; i<5; i++) {
    var button = document.getElementById('button_'+(i+1));
    button.onclick = function() {
       var num = i;
       return function() {
         alert('私は' + (num+1) + '番目のボタンです');
       }
    }
  }
}
</script>

……これでは結果は同じでやはり6番目と表示されてしまう。冷静によく見てみれば、前者のコードと実質等価なのだから当たり前だ。

1時間ほど悩んで、下のような解答をひねり出した。

<script type="text/javascript">
function init() {
  for(var i=0; i<5; i++) {
    var button = document.getElementById('button_'+(i+1));
    var handler = function(n) {
      return function() { alert('私は' + (n+1) + '番目のボタンです'); }
    };
    button.onclick = handler(i);
  }
}
</script>

関数呼び出しはその場で評価されるので、引数を渡してコールしてしまえばよいというわけ。

さて解決した後もなんだか釈然としない。こんな簡単な事で1時間悩んでしまったのは

  1. 自分がバカ。
  2. 何をやるにも困難が待ち受けている世界だからしょうがない。
  3. もっと簡単な方法があるのに気づいていない。