2

Mozila開発者ネットワークで説明されているクロージャの一般的なエラーに遭遇します

期待どおりに動作しません。どの分野に注目しても、年齢に関するメッセージが表示されます。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

それは解決策を与えます。これは別のクロージャーを追加することです。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp(); 

MDNによる説明は、2番目のソリューションが3つの環境を作成したためです。

誰かが私に説明を与えることができますか、フェーズ「環境」は何を意味しますか、そしてどのようにその範囲を知ることができますか?

4

2 に答える 2

3

関数で変数名を使用すると、最初に JavaScript インタープリターがその変数を関数内のローカル変数としてチェックします。そのようなローカル変数が存在しない場合は、外側のスコープまでトラバースして変数を見つけます。(関数が定義されると、スコープ チェーン (定義されている外側の関数のスコープから始まり、その関数を含む関数などに続くスコープの階層) にアクセスできます )

ここで、無名onfocusハンドラー関数は変数 identifier を使用しますitem。は関数内のローカル変数ではないため、ハンドラーが実行されると、JavaScript はで定義されitemている次の外側のスコープをトラバースする必要があります。項目ごとに 1 つのハンドラー関数を定義しました。ただし、これらのハンドラー関数はすべて、外側のスコープで同じ参照を保持します。これらの関数のいずれかが呼び出されるたびに、JavaScript は に対して同じ値を検索します。これは、ループが完了したときに保持していた値です。itemsetupHelpitemitemfor

追加のクロージャがある場合、 の現在の値をitem.help引数として に渡しmakeHelpCallbackます。これはハンドラ関数を返します。JavaScript は変数を値で渡すため、これらのハンドラー関数のそれぞれに、異なる値のhelp.

于 2013-01-01T22:01:37.147 に答える
2

もっと単純な例を考えてみましょう:

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', function() {
        document.getElementById('elem' + i).style.display = 'none';
    });
}

コードは単純に見えますよね?divをクリックすると非表示になるようです。

しかし、それは起こりません。イベント ハンドラは の周りで閉じられiます。iハンドラーの外側で発生することは、ハンドラーiの内側でも発生します (少し単純化するため)。

この動作の例を次に示します。どの div をクリックしても、ELEM #10常に非表示になることに注意してください。i(ループ条件が失敗する前に最後に 1 回インクリメントされたため、9 ではなく 10です。)


魅力的な修正は、これを試すことです:

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', function() {
        var fix = i;
        document.getElementById('elem' + fix).style.display = 'none';
    });
}​

iただし、 is alreadyになるまで割り当てが処理されないため、これは機能しません10。(技術的には、クリック イベントが発生するまで処理されません。その場合iは 10 であると想定されますが、理論的には、すべてのイベントがバインドされる前にクリックが発生iし、その時点で 10 未満になる可能性があります。)


インラインで使用される MDN のトリックを実際に実行できます。

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', (function(j) { return function() {
        document.getElementById('elem' + j).style.display = 'none';
    }}(i)));
}​

変数を関数に渡すと、別の「環境」と見なされます。関数のj内部は、関数が呼び出された時点で凍結されています。内側のクロージャーはその j を使用します。興味深いことに、iこれらの関数の両方の内部にまだ存在しています。アクセスされた時期に応じて、 と の間のどこかに0なり10ます。

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', (function(j) { 
        alert(i); //whichever value is being bound
        //note that i === j is always true here
        //j is a copy of i though and thus stops changing when i changes in the future 
        return function() {
            alert(i); //always 10
            document.getElementById('elem' + j).style.display = 'none';
        };
    }(i)));
}​

JS スコープを考える簡単な方法は、ネストされた関数が親スコープを継承することです (これは本当です)。関数が呼び出されると、変数は呼び出しスコープ内でそのまま渡されます。

ここで、プリミティブは値によって渡され、オブジェクトは参照によって渡されるという興味深い点があります。

var x = 5; (function(n) { ++n; }(x)); /* x is still 5 */
var o = {foo: 'bar'}; (function(obj) { obj.far = 'baz'; }(o)); /* o === {foo: 'bar', far: 'baz'} */ 
于 2013-01-01T22:03:02.013 に答える