46

最近、関数の配列を作成する必要があることに気付きました。関数は XML ドキュメントの値を使用し、for ループを使用して適切なノードを実行しています。しかし、これを行ったところ、XML シートの最後のノード (for ループの最後の実行に対応する) だけが、配列内のすべての関数によって使用されていることがわかりました。

以下は、これを示す例です。

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

window.alert("Num: " + numArr[5] + "\nFun: " + funArr[5]());

出力は Num: 5 および Fun: 10 です。

調査の結果、機能するコードのセグメントを見つけましたが、なぜそれが機能するのかを正確に理解するのに苦労しています。ここで例を使用して再現しました。

var funArr2 = [];
for(var i = 0; i < 10; ++i)
    funArr2[funArr2.length] = (function(i){ return function(){ return i;}})(i);

window.alert("Fun 2: " + funArr2[5]());

スコーピングに関係していることはわかっていますが、一見したところ、私の単純なアプローチとは異なる動作をするようには見えません。私は Javascript の初心者なので、この関数を関数に返す手法を使用すると、スコープの問題が回避されるのはなぜですか? また、最後に (i) が含まれるのはなぜですか?

事前にどうもありがとうございました。

4

3 に答える 3

42

ループ変数名をマスクしないパラメーター名を使用する場合、2 番目の方法は少し明確になります。

funArr[funArr.length] = (function(val) { return function(){  return val; }})(i);

現在のコードの問題は、各関数がクロージャであり、それらがすべて同じ変数を参照していることですi。各関数が実行iされると、関数が実行された時点の の値が返されます (これは、ループの制限値よりも 1 つ大きくなります)。

より明確な方法は、必要なクロージャーを返す別の関数を作成することです。

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = getFun(i);
}

function getFun(val) {
    return function() { return val; };
}

これは基本的に、私の回答のコードの最初の行と同じことを行っていることに注意してください。関数を返す関数を呼び出し、の値をiパラメーターとして渡します。その主な利点は明快さです。

編集: EcmaScript 6 がほぼすべての場所でサポートされるようになったので (申し訳ありませんが、IE ユーザー)、ループ変数のlet代わりにキーワードを使用して、より簡単な方法で対処できます。var

var numArr = [];
var funArr = [];
for(let i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

その小さな変更により、各要素は、ループの反復ごとに異なるfunArrオブジェクトを実行するクロージャ バウンドになります。の詳細については、この Mozilla Hacks の 2015 年の投稿を参照してください。( をサポートしていない環境をターゲットにしている場合は、以前に書いたものに固執するか、使用する前にこれを最後にトランスパイラーで実行してください。 iletlet

于 2013-10-30T23:54:16.393 に答える
7

コードが何をするのかをもう少し詳しく調べて、架空の関数名を割り当ててみましょう。

(function outer(i) { 
    return function inner() { 
        return i;
    }
 })(i);

ここでouter、引数を受け取りますi。JavaScript は関数スコープを採用しています。つまり、各変数は、それが定義されている関数内にのみi存在outerouterます。

inner変数 への参照が含まれていますi。(パラメータとして、またはキーワードを使用して再定義しないことに注意してください!) JavaScript のスコープ規則では、そのような参照は、最初に囲んでいるスコープ (ここでは のスコープ) に結び付ける必要があると規定されています。したがって、内は 内 にあったものを指します。ivarouteriinneriouter

最後に、 function を定義した後、すぐにそれを呼び出し、値(最も外側のスコープで定義された別の変数)outerを渡します。iiは で囲まれてouterおり、その値は最も外側のスコープ内のどのコードでも変更できなくなりました。したがって、ループ内で最も外側iがインクリメントされると、内側は同じ値を保持します。foriouter

それぞれが独自のスコープと引数値を持つ多くの無名関数を実際に作成したことを思い出すと、これらの無名関数のそれぞれがi.

最後に、完全を期すために、元のコードで何が起こったのかを調べてみましょう。

for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

ここで、無名関数に最も外側の への参照が含まれていることがわかりますi。その値が変更されると、無名関数内に反映されますが、値の独自のコピーはいかなる形式でも保持されません。したがって、i == 10作成したすべての関数を呼び出した時点で最も外側のスコープにあるため、各関数は value を返します10

于 2013-10-31T00:00:17.653 に答える
6

このような一般的な落とし穴を回避できるように、 JavaScript: The Definitive Guide のような本を手に取って JavaScript の一般的な理解を深めることをお勧めします。

この回答は、具体的にはクロージャーに関する適切な説明も提供します。

JavaScript クロージャーはどのように機能しますか?

呼び出すとき

function() { return i; }

関数は実際には、i が定義されている親呼び出しオブジェクト (スコープ) で変数ルックアップを行っています。この場合、i は 10 として定義されているため、これらの関数はすべて 10 を返します。これが機能する理由

(function(i){ return function(){ return i;}})(i);

つまり、無名関数をすぐに呼び出すことによって、現在の iが定義されている新しい call-object が作成されます。したがって、ネストされた関数を呼び出すと、その関数は、i が最初に定義されたスコープ (まだ 10) ではなく、無名関数の呼び出しオブジェクト (呼び出されたときに渡した値を定義する) を参照します。 .

于 2013-10-31T01:26:29.620 に答える