web-dev-qa-db-ja.com

ループ内に「クリック」イベントリスナーを追加する

Htmlタグ内の標準onClickをリスナーにリファクタリングすると、私のコードで問題が発生します:

var td;
    for (var t=1;t<8;t++){
        td = document.getElementById('td'+t);
        if (typeof window.addEventListener==='function'){
                td.addEventListener('click',function(){
                    console.log(td);
            })}
 }  

td要素をクリックすると、ループの最後のインデックスでtdがクリックされたと想定されます。 7
eventListenersのように見えますが、このループの最後の要素のみに入力されています。
ループの初期化は正しいようです。
なぜそうなったのですか?

これが ライブコード です

21
sergionni

イベントリスナーの割り当てをクロージャーでラップする必要があります。

var td;
for (var t = 1; t < 8; t++){
    td = document.getElementById('td'+t);
    if (typeof window.addEventListener === 'function'){
        (function (_td) {
            td.addEventListener('click', function(){
                console.log(_td);
            });
        })(td);
    }
}
58
jabclab

何が起こっていますか

変数tdはイベントハンドラーの外部で定義されているため、セルをクリックすると、最後に設定された値が記録されます。

より技術的には、各イベントハンドラー関数はクロージャー、つまり外部スコープから変数を参照する関数です。

一般的な解決策

この種の問題の一般的な解決策は、ラッパー関数からイベントハンドラーを返し、引数として「修正」する変数を渡すことです。

td.addEventListener('click', function(wrapTD) {
    return function() { console.log(wrapTD); }
}(td));

引数は、呼び出されたラッパー関数のスコープにバインドされます。

より簡単な解決策:thisを使用します

ただし、さらに簡単なオプションがあります。イベントハンドラーでは、thisはハンドラーが定義されている要素に設定されますなので、thisの代わりにtdを使用できます。

td.addEventListener('click', function() {
    console.log(this);
});

さらにシンプル:ループなし!

最後に、forループを完全に取り除くことができますで、テーブル全体に単一のイベントハンドラーを設定します。

var table = document.getElementById('my-table');

table.addEventListener('click', function(e) {
    if (e.target.nodeName.toUpperCase() !== "TD") return;

    var td = e.target;
    console.log(td);
});

これは、複数のイベントハンドラーを1つだけに置き換えるため、大規模なテーブルにははるかに優れたソリューションです。テキストを別の要素でラップする場合、ターゲット要素がtdの子孫であるかどうかを確認するためにこれを適応させる必要があることに注意してください。

34
Jordan Gray

したがって、ここで起こっているのは、イベントリスナー関数のスコープに変数「td」を保持しているということです。 'td'のインスタンスは1つしかなく、forループが繰り返されるたびに更新されます。したがって、forループが終了すると、tdの値は要素「#td7」に設定され、イベントハンドラーは単にtdの現在の値をログに記録します。

上記の例では、単純に「this」をログに記録できます。

var td;
for (var t=1;t<8;t++){
    td = document.getElementById('td'+t);
    if (typeof window.addEventListener==='function'){
      td.addEventListener('click',function(){
        console.log(this);
      });
    }
}

「this」が要素に設定されているため、イベントハンドラーの実行のためにイベントがバインドされました。

Forループ内からクロージャーを作成するときにイテレーターを保持することについて、もっと多くの答えを探していると思います。これを行うには、forループの外で関数を定義します。

for (var t=1;t<8;t++){
  bind_event(t);
}

function bind_event(t) {
    var td = document.getElementById('td'+t);
    if (typeof window.addEventListener==='function'){
      td.addEventListener('click',function(){
        console.log(td);
      });
    }
}

このように、bind_eventが実行されるたびに「td」という名前の変数のインスタンスが作成され、そのインスタンスはイベントリスナー関数のクロージャー内に保持されます。 bind_eventの「t」も新しい変数であることは注目に値します。

8
Will Tomlins

私が理解しているように、それは閉鎖のために発生します... FORステートメントのスコープ内でイベントハンドラーを割り当てます。クリックが発生すると、TD変数がFORのスコープ内にある場合、最後のバージョンが取得され、ログに書き込まれます。

1
Alex Dn

以下は期待どおりに機能するはずです。

  for (var t=1;t<8;t++){
        var td;
        td = document.getElementById('td'+t);
        if (typeof window.addEventListener==='function'){
                td.addEventListener('click',function(){
                    console.log(this);
            })}
 } 
0
Evert