somemo programming etc.

プログラマ、雑記、プログラミング関係はLinkから、数式はこっちでまとめていることが多い

【Javascript】ループしながらイベント設定【変数バインド】

ループしながらイベント設定をする方法のメモです。

ボタンを用意

今回は、3つの方法で試します。それぞれの方法につきボタンを3つ使います。

var i, j, num = 3;
var buttons_ol = document.getElementById('buttons_test');
for (i = 0; i < num; i++) {
  // リスト要素の作成
  var li = document.createElement('li');
  li.id = 'buttons' + i;

  // ボタンの作成
  for (j = 0; j < num; j++) {
    var input = document.createElement('input');
    input.type = 'button';
    input.id = 'btn_' + i + '_' + j;
    input.value = 'btn_' + i + '_' + j;
    li.appendChild(input);
  }

  buttons_ol.appendChild(li);
}

単純ループ設定

まず、単純にループで設定する方法です。ぱっと頭に浮かぶやり方はこれだと思いますが、間違っています。Javascriptの仕様を把握していないと越えられない第一関門だと思います。

var buttons = document.getElementById('buttons0')
                      .getElementsByTagName('input');

var i, len;
for (i = 0, len = buttons0.length; i < len; i++) {
  if (buttons0[i].addEventListener) {
    button0[i].addEventListener('click', function() {
      alert('this.value is ' + this.value + ', i is ' + i);
    });
  }
}

クリック時、this.valueは期待通りに動きます。buttons[i]のスコープに存在するため、thisはbuttons[i]をあらわしています。また、この時点ではbuttons[i]は評価されているので、#buttonsの子要素のボタンであることが分かっています。

しかし、変数iの値はどのボタンでも3となります。そもそもクリック時に実行されるということは、まだ関数の内容を評価していませんので、iはiのままとなっています。

これは、iの値を保持している・割り当てられているのではなく、iの参照先を保持していると考えられます。また、クリック時に割り当てられた関数のスコープをみると、ここには変数iは存在しません。よってJavascriptの仕様上、上位のスコープから変数iを探すことになります。

上位のスコープをみると、最上位のスコープ(グローバルスコープ)に存在します。iはfor文により3までインクリメントされているため、3と表示されるのは当たり前ですね。また、Javascriptではforのスコープは存在しないので、forの初期化処理でvar宣言してもfor文の属するスコープで値を初期化しない状態でvar宣言していることになります。

即時実行関数による設定

即時実行関数による設定です。変数や引数に着目するとわかるでしょう。

var buttons1 = document.getElementById('buttons1')
                       .getElementsByTagName('input');

var j, len1, id_num = 100;
for (j = 0, len1 = buttons1.length; j < len1; j++) {
  if (buttons1[j].addEventListener) {
    (function(id_num) {
      buttons1[j].addEventListener('click', function() {
        alert('this.value is ' + this.value + ', id_num is ' + id_num);
      });
    })(j);
  }
}

イベント設定部分を即時実行関数で行っています。

まず、即時実行関数の引数に変数jを渡しています。これにより、渡された変数jと引数id_num引数は別物とみなされます。引数ですので、もちろんグローバルスコープに用意していたid_numとの区別もついています。関数設定時には毎回違う値となっているので、それぞれのボタンをクリックした際にも違う値となります。

関数を返す関数による設定

関数を返す関数による設定です。こちらも変数や引数に着目するとわかるでしょう。

var buttons2 = document.getElementById('buttons2')
                       .getElementsByTagName('input');

var j, len2, id_num = 100;
for (j = 0, len2 = buttons2.length; j < len2; j++) {
  if (buttons2[j].addEventListener) {
    buttons2[j].addEventListener('click', function(id_num) {
      return function() {
        alert('this.value is ' + this.value + ', id_num is ' + id_num++);
      }
    }(j));
  }
}

クリック時に行う関数を設定する引数部分で、関数を即時実行して関数を返すように設定しています。こちらも2番目と内容ほぼ同じです。少し変えた点は、クリック時にid_numをインクリメントしているところです。クリックし続けると、値が増えていきます。また、他のボタンをクリックしても値が違うことから、それぞれのid_numには影響がないこともわかります。

Javascriptではスコープに気をつけなくてはならないことがよくわかるいい問題でした。