web-dev-qa-db-ja.com

Javascriptの悪名高いループの問題?

次のコードスニペットがあります。

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function () {
            alert(i);
        };
        document.body.appendChild(link);
    }
}

上記のコードは、5つのリンクを生成し、各リンクをアラートイベントにバインドして、現在のリンクIDを表示するためのものです。しかし、それは機能しません。生成されたリンクをクリックすると、すべて「link 5」と表示されます。

ただし、次のコードスニペットは期待どおりに機能します。

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function (num) {
            return function () {
                alert(num);
            };
        }(i);
        document.body.appendChild(link);
    }
}

上記の2つのスニペットは here から引用されています。著者の説明として、closureが魔法を作るようです。

しかし、それがどのように機能し、どのようにclosureが機能するかはすべて私の理解を超えています。なぜ最初のものは機能せず、2番目のものは機能しないのですか?誰もが魔法について詳しく説明できますか?

ありがとう。

216
Zhu Tao

自分自身を引用する 最初の例の説明:

JavaScriptのスコープはブロックレベルではなく関数レベルであり、クロージャーを作成するということは、囲むスコープが囲まれた関数の字句環境に追加されることを意味します。

ループが終了すると、関数レベルの変数iの値は5になります。これは、内部関数が「見る」ものです。

2番目の例では、各反復ステップで、外部関数リテラルは、独自のスコープとローカル変数numを持つ新しい関数オブジェクトに評価され、その値はiの現在の値に設定されます。 numは変更されないため、クロージャの有効期間中は一定のままです。関数オブジェクトは独立しているため、次の反復ステップでは古い値が上書きされません。

リンクごとに2つの新しい関数オブジェクトを作成する必要があるため、このアプローチはかなり効率が悪いことに注意してください。情報の保存にDOMノードを使用すると簡単に共有できるため、これは不要です。

function linkListener() {
    alert(this.i);
}

function addLinks () {
    for(var i = 0; i < 5; ++i) {
        var link = document.createElement('a');
        link.appendChild(document.createTextNode('Link ' + i));
        link.i = i;
        link.onclick = linkListener;
        document.body.appendChild(link);
    }
}
105
Christoph

私は厚いので、ここに行くので、私は厚い人のために簡単な説明を書くのが好きです...

ページには5つのdivがあり、それぞれにID ... div1、div2、div3、div4、div5があります

jQueryはこれを行うことができます...

for (var i=1; i<=5; i++) {
    $("#div" + i).click ( function() { alert ($(this).index()) } )
}

しかし、実際に問題に対処します(そして、これをゆっくりと構築します)...

ステップ1

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        // TODO: Write function to handle click event
    )
}

ステップ2

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        function(num) {
            // A functions variable values are set WHEN THE FUNCTION IS CALLED!
            // PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
            // Now the click event is expecting a function as a handler so return it
            return function() { alert (num) }
        }(i) // We call the function here, passing in i
    )
}

簡単に代替を理解する

あなたがそれを回避できない場合、これは理解しやすく、同じ効果があります...

for (var i=1; i<=5; i++) {

    function clickHandler(num) {    
        $("#div" + i).click (
            function() { alert (num) }
        )
    }
    clickHandler(i);

}

これは、関数が呼び出されたときに関数変数値が設定されていることを覚えている場合、理解しやすいはずです(ただし、これは以前とまったく同じ思考プロセスを使用します)

78
Daniel Lewis

基本的に、最初の例では、iハンドラー内のonclickiハンドラー外のonclickに直接バインドしています。したがって、iハンドラー外のonclickが変更されると、iハンドラー内のonclickも変更されます。

2番目の例では、numハンドラーのonclickにバインドする代わりに、関数に渡して、numonclickにバインドします。 ] _ハンドラー。関数に渡すとき、iの値はコピー、バインドされていない to numです。したがって、iが変更されても、numは変わりません。 JavaScriptの関数は「クロージャー」であるため、コピーが発生します。つまり、関数に何かが渡されると、外部の変更のために「クローズ」されるためです。

20
Imagist

何が起こっているかを説明している人もいますが、代替ソリューションがあります。

function addLinks () {
  for (var i = 0, link; i < 5; i++) {
    link = document.createElement("a");
    link.innerHTML = "Link " + i;

    with ({ n: i }) {
      link.onclick = function() {
        alert(n);
      };
    }
    document.body.appendChild(link);
  }
}

基本的に貧しい人たちは拘束力があります。

17
nlogax

最初の例では、この関数をonclickイベントに単純にバインドします。

function() {alert(i);};

これは、クリックイベントで、jsがaddlink関数i変数の値を警告することを意味します。 for loop()のため、その値は5になります。

2番目の例では、別の関数にバインドされる関数を生成します。

function (num) {
  return function () { alert(num); };
}

つまり、値を指定して呼び出された場合、入力値を警告する関数を返します。例えば。 function(3)を呼び出すと、function() { alert(3) };が返されます。

繰り返しごとに値iでこの関数を呼び出すため、リンクごとに個別のonclick関数を作成します。

ポイントは、最初の例では関数に変数参照が含まれていましたが、2番目の例では外部関数の助けを借りて参照を実際の値に置き換えました。これは、関数への参照を保持するのではなく、関数内の変数の現在の値を「囲む」ため、大まかにクロージャーと呼ばれます。

5
Zed