ループ内のJavaScriptクロージャー–簡単で実用的な例


2818

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

これは出力します:

私の価値:3
私の価値:3
私の価値:3

それを出力したいのですが:

私の値:0
私の値:1
私の値:2


関数の実行の遅延がイベントリスナーの使用が原因である場合にも、同じ問題が発生します。

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>

…または非同期コード、例えばPromisesの使用:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

編集:それはまたループ内for inで明らかですfor of

const arr = [1,2,3];
const fns = [];

for(i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(f of fns){
  f();
}

この基本的な問題の解決策は何ですか?


55
funcs数値インデックスを使用している場合、配列になりたくないですか?頭を上げただけです。
DanMan 2013

23
これは本当に混乱する問題です。この記事は私がそれを理解するのに役立ちます。他の人にも役立つかもしれません。
user3199690 2014年

4
別の簡単で説明のあるソリューション:1)ネストされた関数は、それらの「上の」スコープにアクセスできます。2)閉鎖ソリューション ...「クロージャは、親機能が閉じた後でも、親スコープへのアクセス権を持つ関数です」。
Peter Krauss、2014


35
ES6、自明な解は、変数を宣言することであるIを有するLETループの本体にスコープされ、。
Tomas Nikodym

回答:


2148

さて、問題は、i各無名関数内の変数が、関数の外で同じ変数にバインドされていることです。

従来のソリューション:クロージャー

あなたがしたいことは、各関数内の変数を、関数の外の独立した変化しない値にバインドすることです:

var funcs = [];

function createfunc(i) {
  return function() {
    console.log("My value: " + i);
  };
}

for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

JavaScriptにはブロックスコープがなく、関数スコープだけがあるため、関数の作成を新しい関数でラップすることにより、「i」の値が意図したとおりに維持されるようにします。


ES5.1ソリューション:forEach

Array.prototype.forEach(2015年に)関数が比較的広く利用できるようになっているので、主に値の配列に対する反復を含む状況で.forEach()は、反復ごとに明確なクロージャーを取得するためのクリーンで自然な方法が提供されます。つまり、値(DOM参照、オブジェクトなど)を含むある種の配列があり、各要素に固有のコールバックの設定で問題が発生すると想定すると、次のようになります。

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

アイデアは、.forEachループで使用されるコールバック関数の呼び出しごとに独自のクロージャーになるというものです。そのハンドラーに渡されるパラメーターは、反復の特定のステップに固有の配列要素です。非同期コールバックで使用された場合、反復の他のステップで確立された他のコールバックと衝突しません。

たまたまjQueryで作業している場合、この$.each()関数は同様の機能を提供します。


ES6ソリューション: let

ECMAScript 6(ES6)は、ベースの変数とは異なるスコープの新しいキーワードletconstキーワードを導入していvarます。たとえば、letベースのインデックスを持つループでは、ループの各反復iはループスコープを持つ新しい変数を持つため、コードは期待どおりに機能します。多くのリソースがありますが、2alityのブロックスコーピングの投稿を優れた情報源としてお勧めします。

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

ただし、Edge 14より前のIE9-IE11とEdgeはサポートしてletいますが、上記のエラーが発生することに注意してください(i毎回新しいものを作成しないため、上記のすべての関数は、を使用した場合と同様に3をログに記録しますvar)。エッジ14でようやく正しくなりました。


7
function createfunc(i) { return function() { console.log("My value: " + i); }; }変数を使用しているため、まだクロージャではありませんiか?
アレックス

55
残念ながら、この答えは時代遅れであり、誰もが一番下に正しい答えを見ることはありません- Function.bind()今のところ使用することは間違いなく好ましいです、stackoverflow.com / a / 19323214/785541を参照してください。
Wladimir Palant 2014年

81
@Wladimir:あなたの提案.bind()では、「正しい答えは」右ではありません。彼らはそれぞれ自分の場所を持っています。では.bind()this値をバインドせずに引数をバインドすることはできません。またi、必要に応じて、呼び出し間で引数を変更する機能なしで引数のコピーを取得します。ですから、.bind()実装は歴史的に遅いことは言うまでもありません。確かに単純な例でも機能しますが、クロージャーは理解すべき重要な概念であり、それが問題でした。
クッキーモンスター

8
これらのfor-return関数ハックの使用を停止してください。同じスコープ変数の再利用を回避するため、代わりに[] .forEachまたは[] .mapを使用してください。
Christian Landgren、2015

32
@ChristianLandgren:これは、配列を反復する場合にのみ役立ちます。これらのテクニックは「ハッキング」ではありません。それらは本質的な知識です。

379

試してください:

var funcs = [];
    
for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}

編集(2014):

個人的には、@ Austの使用に関する最近の回答.bindが、現在この種のことを行うための最良の方法だと思います。LO-ダッシュをアンダースコアだ/もあります_.partialあなたが必要とするかを台無しにしたくないときbindさんthisArg


2
についての説明}(i));
aswzen

3
@aswzen 関数へのi引数として渡されると思いindexます。
ジェットブルー

ローカル変数インデックスを実際に作成しています。
Abhishek Singh

1
IIFEとも呼ばれる関数式をすぐに呼び出します。(i)は無名関数式への引数であり、すぐに呼び出され、インデックスはiから設定されます。

348

まだ言及されていない別の方法は、 Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

更新

@squintと@mekdevで指摘されているように、最初にループの外側で関数を作成してから、ループ内で結果をバインドすると、パフォーマンスが向上します。

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


これも私が最近行っていることで、lo-dash / underscoreも好きです_.partial
ビョルン

17
.bind()ECMAScript 6の機能の大部分は廃止されます。さらに、これは実際には反復ごとに2つの関数を作成します。最初に匿名、次にによって生成されたもの.bind()。ループの外側で作成してから、.bind()内側で作成するのが適切です。

5
@squint @mekdev-あなたはどちらも正しいです。私の最初の例は、がどのようbindに使用されるかを示すためにすばやく作成されました。私はあなたの提案ごとに別の例を追加しました。
Aust

5
2つのO(n)ループで計算を無駄にする代わりに、(var i = 0; i <3; i ++){log.call(this、i); }
user2290820

1
.bind()は、受け入れられた回答がPLUSをいじることを示唆することを行いthisます。
niry

269

インデックス変数を囲む最も簡単で最も読みやすい方法である、即時に呼び出される関数式を使用します。

for (var i = 0; i < 3; i++) {

    (function(index) {

        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value:   $.ajax({});
    
    })(i);

}

これにより、イテレータiが匿名関数に送信されindexます。これによりクロージャーが作成さiれ、IIFE内の非同期機能で後で使用するために変数が保存されます。


10
さらにコードを読みやすくするため、およびどちらiが何であるかに関する混乱を避けるために、関数パラメーターの名前をに変更しますindex
カイルファルコナー2014年

5
この手法を使用して、元の質問で説明されている配列関数をどのように定義しますか
ニコ

@Nicoのindex代わりにを使用することを除いて、元の質問で示した方法と同じですi
JLRishe 2015年

@JLRishevar funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }
ニコ

1
@NicoでOPの特定のケースでは、彼らはただの数字を繰り返し処理しているので、これがために偉大なケースではないでしょう.forEach()1が配列でオフに開始されたとき、しかし、多くの時間、forEach()のように、良い選択であります:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
JLRishe

164

パーティーに少し遅れましたが、私は今日この問題を調査していて、答えの多くがJavaScriptがスコープをどのように扱うかを完全に扱っていないことに気づきました。

他の多くの人が述べたように、問題は内部関数が同じi変数を参照していることです。では、反復ごとに新しいローカル変数を作成し、代わりにその内部関数参照を用意しないのはなぜでしょうか。

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

前と同じように、各内部関数はに割り当てられた最後の値を出力しましたがi、今度は各内部関数がに割り当てられた最後の値を出力しilocalます。しかし、各イテレーションに独自のものがあるべきではありませんilocalか?

結局のところ、それが問題です。各反復は同じスコープを共有しているため、最初の反復後のすべての反復は単に上書きされますilocalMDNから:

重要:JavaScriptにはブロックスコープがありません。ブロックで導入された変数は、それを含む関数またはスクリプトにスコープが設定され、それらの設定の効果はブロック自体を超えて持続します。つまり、ブロックステートメントはスコープを導入しません。「スタンドアロン」ブロックは有効な構文ですが、JavaScriptでスタンドアロンブロックを使用することは望ましくありません。CまたはJavaでそのようなブロックのようなことを行うと思われる場合、スタンドアロンブロックは意図したとおりに機能しないためです。

強調のために繰り返し:

JavaScriptにはブロックスコープがありません。ブロックで導入された変数のスコープは、含まれている関数またはスクリプトです

ilocalこれは、各反復で宣言する前に確認することで確認できます。

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

これが、このバグが非常に扱いにくい理由です。変数を再宣言している場合でも、JavaScriptはエラーをスローせず、JSLintは警告をスローしません。これはまた、これを解決する最善の方法がクロージャを利用することである理由でもあります。これは基本的に、JavaScriptでは、内部スコープが外部スコープを「囲む」ため、内部関数が外部変数にアクセスできるという考え方です。

閉鎖

これは、内部関数が外部変数を「保持」し、外部関数が戻った場合でもそれらを維持することも意味します。これを利用するには、純粋にラッパー関数を作成して呼び出し、新しいスコープを作成し、新しいスコープで宣言ilocalして、使用する内部関数を返しますilocal(詳細については以下を参照)。

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

ラッパー関数内に内部関数を作成すると、内部関数に、それだけがアクセスできるプライベート環境、つまり「クロージャー」が与えられます。したがって、ラッパー関数を呼び出すたびに、独自の独立した環境で新しい内部関数を作成し、ilocal変数が互いに衝突したり上書きしたりしないようにします。いくつかのマイナーな最適化は、他の多くのSOユーザーが与えた最終的な答えを与えます:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

更新

ES6が主流になったので、新しいletキーワードを使用して、ブロックスコープの変数を作成できます。

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

とても簡単です。詳細については、この情報に基づいて私の回答を参照してください。


IIFEの方法を説明した方法も気に入っています。それを探していました。ありがとうございました。
CapturedTree

4
letconstキーワードを使用したJavaScriptでのブロックスコープのようなものがあります。この回答がそれを含めるように拡張された場合、それは私の意見ではよりグローバルに役立つでしょう。

@TinyGiant確かに、私はいくつかの情報を追加letし、より完全な説明をリンクしました
woojoo666

@ woojoo666答えは、次のようにループで2つの交互のURLを呼び出す場合にも機能しますi=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }か?(window.open()をgetelementbyid ......に置き換えることができます。)
nattyについての

@nuttyaboutnatty返信が遅くなってすみません。あなたの例のコードはすでに機能しているようではありません。あなたは使用していないiあなたが閉鎖を必要としないので、あなたのタイムアウト機能で
woojoo666

151

ES6が広くサポートされるようになり、この質問に対する最良の答えが変わりました。ES6は、この正確な状況に対応するletおよびconstキーワードを提供します。クロージャをいじる代わりに、次のletようにしてループスコープ変数を設定できます。

var funcs = [];

for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value: " + i); 
    };
}

val次に、ループの特定のターンに固有のオブジェクトをポイントし、追加のクロージャー表記なしで正しい値を返します。これは明らかにこの問題を大幅に簡素化します。

constlet最初の割り当て後に変数名を新しい参照に再バインドできないという追加の制限と同様です。

最新バージョンのブラウザーを対象とするユーザー向けに、ブラウザーのサポートが提供されています。const/ letは現在、最新のFirefox、Safari、Edge、Chromeでサポートされています。Nodeでもサポートされており、Babelのようなビルドツールを利用することで、どこでも使用できます。ここで実際の例を見ることができます: http //jsfiddle.net/ben336/rbU4t/2/

ここのドキュメント:

ただし、Edge 14より前のIE9-IE11とEdgeはサポートしてletいますが、上記のエラーが発生することに注意してください(i毎回新しいものを作成しないため、上記のすべての関数は、を使用した場合と同様に3をログに記録しますvar)。エッジ14でようやく正しくなりました。


残念ながら、「let」は、特にモバイルでは、まだ完全にはサポートされていません。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
MattC

2
let 'は、June '16の時点で、iOS Safari、Opera Mini、Safari 9を除くすべての主要なブラウザーバージョンでサポートされています。Evergreenブラウザーがサポートしています。Babelはそれを正しくトランスパイルして、高コンプライアンスモードをオンにせずに期待される動作を維持します。
Dan Pantry

@DanPantryはい、更新の時間についてです:) const、docリンク、および互換性情報の言及を追加するなど、現在の状況をよりよく反映するように更新されました。
ベンマコーミック2016年

ES6 / 7をサポートしないブラウザーが何が起こっているのかを理解できるように、コードをトランスパイルするためにバベルを使用するのはこのためではないでしょうか。
ピクセル67

87

別の言い方をすると、 i、関数内は関数の作成時ではなく、関数の実行時にバインドされるということです。

クロージャーを作成すると、 iはコピーではなく、外部スコープで定義された変数への参照です。実行時に評価されます。

他のほとんどの答えは、値を変更しない別の変数を作成することによって回避する方法を提供します。

わかりやすくするために説明を追加すると思いました。解決策としては、個人的には、ここでの回答から説明する最もわかりやすい方法であるHarto'sを使用します。投稿されたコードはどれでも機能しますが、新しい変数(Freddyおよび1800's)を宣言している理由や奇妙な埋め込みクロージャー構文(apphacker)を宣言している理由を説明するために、コメントの山を書く必要はなく、クロージャーファクトリーを選びます。


71

理解する必要があるのは、JavaScriptの変数のスコープが関数に基づいていることです。これは、ブロックスコープがあり、変数をforの内部にコピーするだけで機能するc#と言うよりも重要な違いです。

変数が関数スコープを持つようになったので、apphackerの答えのように関数を返すことを評価する関数でそれをラップするとトリックが実行されます。

varの代わりにletキーワードもあり、ブロックスコープルールを使用できます。その場合、for内で変数を定義するとうまくいきます。とはいえ、letキーワードは互換性があるため、実用的なソリューションではありません。

var funcs = {};

for (var i = 0; i < 3; i++) {
  let index = i; //add this
  funcs[i] = function() {
    console.log("My value: " + index); //change to the copy
  };
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


@nickfどのブラウザですか?言ったように、互換性の問題があります。つまり、IEでletがサポートされているとは思えないような、深刻な互換性の問題です。
eglasius 2009

1
@nickfはい、次のリファレンスを確認してください:developer.mozilla.org/En/New_in_JavaScript_1.7 ... let定義セクションを確認してください。ループ内にonclickの例があります
eglasius '16

2
@nickfうーん、実際にはバージョンを明示的に指定する必要があります:<script type = "application / javascript; version = 1.7" /> ... IEの制限のため、実際にはどこにも使用していません。実用的:(
eglasius 2009



59

これは、Bjornの(apphacker)と同様の手法の別のバリエーションです。これにより、変数値をパラメーターとして渡すのではなく、関数内に割り当てることができます。

var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() {
        var index = i;
        return function() {
            console.log("My value: " + index);
        }
    })();
}

どのような手法を使用しても、index変数は一種の静的変数になり、返される内部関数のコピーにバインドされます。つまり、その値の変更は呼び出し間で保存されます。とても便利です。


おかげで、あなたの解決策は機能します。しかし、なぜこれが機能するのかを尋ねたいのですが、var行を入れ替えるとreturn行が機能しませんか?ありがとう!
midnite 2013

@midniteスワップvarしたreturn場合、変数は内部関数を返す前に割り当てられません。
Boann 2013

53

これは、JavaScriptでクロージャを使用する際の一般的な間違いについて説明しています。

関数は新しい環境を定義します

検討してください:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

makeCounterが呼び出されるたび{counter: 0}に、新しいオブジェクトが作成されます。また、の新しいコピーobj も作成され、新しいオブジェクトを参照します。したがって、counter1およびcounter2は互いに独立しています。

ループ内の閉鎖

ループでクロージャーを使用するのは難しいです。

検討してください:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

そのことに注意してください counters[0]と独立しcounters[1]いない。実際、彼らは同じ上で動作しますobjます!

これはobj、おそらくパフォーマンス上の理由から、ループのすべての反復にわたって共有のコピーが1つしかないためです。にもかかわらず、{counter: 0}各反復で新しいオブジェクトの同じコピーを作成しますobj意志は、単に最新のオブジェクトを参照して更新されます。

解決策は、別のヘルパー関数を使用することです。

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

これは、関数スコープ内のローカル変数と関数引数変数が直接エントリに割り当てられるため、機能します。


小さな説明:ループ内のクロージャの最初の例では、counters [0]とcounters [1]は独立していません。これは、パフォーマンス上の理由によるものではありません。その理由は、var obj = {counter: 0};次のように、コードが実行される前に評価されるためです 。MDN varvar宣言は、コードが実行される前に処理されます。
Charidimos

50

最も簡単な解決策は、

代わりに:

var funcs = [];
for(var i =0; i<3; i++){
    funcs[i] = function(){
        alert(i);
    }
}

for(var j =0; j<3; j++){
    funcs[j]();
}

「2」を3回警告します。これは、forループで作成された無名関数が同じクロージャを共有し、そのクロージャではの値がi同じであるためです。これを使用して共有クロージャを防止します。

var funcs = [];
for(var new_i =0; new_i<3; new_i++){
    (function(i){
        funcs[i] = function(){
            alert(i);
        }
    })(new_i);
}

for(var j =0; j<3; j++){
    funcs[j]();
}

この背後にあるアイデアは、forループの本体全体をIIFE(Immediately-Invoked Function Expression)でカプセル化new_iし、パラメータとして渡してとしてキャプチャすることiです。無名関数はすぐに実行されるので、i内で定義された関数ごとに値が異なります。

このソリューションは、この問題が発生している元のコードに最小限の変更を加える必要があるため、このような問題に適しているようです。実際、これは仕様によるものであり、まったく問題になりません。


2
本で同じようなものを一度読んでください。私もこれを好みます。これは、既存のコードに(それほど)手を触れる必要がないため、自己呼び出し関数のパターンを習得すると、なぜそれを行ったのかが明らかになるためです。範囲。
DanMan 2013

1
@DanManありがとう。自己呼び出しの匿名関数は、JavaScriptのブロックレベル変数スコープの欠如に対処するための非常に良い方法です。
KemalDağ2013

3
自己呼び出しまたは自己呼び出しは、この手法に適した用語ではありません。IIFE(即時呼び出し関数式)の方が正確です。参考:benalman.com/news/2010/11/...
jherax

31

これより短いものを試してください

  • 配列なし

  • 余分なforループはありません


for (var i = 0; i < 3; i++) {
    createfunc(i)();
}

function createfunc(i) {
    return function(){console.log("My value: " + i);};
}

http://jsfiddle.net/7P6EN/


1
あなたの解決策は正しいように出力しているようですが、それは不必要に関数を使用しています、なぜ単にconsole.log出力ではないのですか?元の質問は、同じ閉包を持つ無名関数の作成についてです。問題は、それらが単一のクロージャーを持っているので、iの値がそれらのそれぞれに対して同じであることでした。よろしくお願いします。
KemalDağ2015年

30

以下は、使用する簡単なソリューションですforEach(IE9まで機能します)。

var funcs = [];
[0,1,2].forEach(function(i) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
})
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

プリント:

My value: 0
My value: 1
My value: 2

27

OPによって示されるコードの主な問題iは、2番目のループまで読み込まれないことです。実例として、コード内でエラーが発生することを想像してください。

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

エラーfuncs[someIndex]は、が実行されるまで実際には発生しません()。この同じロジックを使用すると、この値iもこの時点まで収集されないことは明らかです。元のループが終了したら、i++もたらすiの値に3条件をもたらすi < 3失敗と終了ループ。この時点で、iis が使用されて3いる場合funcs[someIndex]()、およびi評価され、それが3である-毎回。

これを乗り越えるiには、遭遇時に評価する必要があります。これはすでにの形で起こっていることに注意してくださいfuncs[i](3つの一意のインデックスがある場合)。この値を取得するにはいくつかの方法があります。1つは、関数としてパラメーターとして渡すことです。これは、既にいくつかの方法で示されています。

別のオプションは、変数を閉じることができる関数オブジェクトを作成することです。このようにして達成できます

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};

23

JavaScript関数は、宣言時にアクセスできるスコープを「閉じ」、そのスコープ内の変数が変更されても、そのスコープへのアクセスを保持します。

var funcs = []

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

上記の配列の各関数は、グローバルスコープ(グローバル、それがたまたま宣言されているスコープであるため)で閉じます。

後でこれらの関数が呼び出さiれ、グローバルスコープ内のの最新の値がログに記録されます。それが閉鎖の魔法であり、欲求不満です。

「JavaScript関数は、宣言されているスコープを閉じ、スコープ内の変数値が変化しても、そのスコープへのアクセスを保持します。」

let代わりにvarを使用すると、forループが実行されるたびに新しいスコープを作成し、関数ごとに個別のスコープを作成して終了します。他のさまざまなテクニックは、追加の関数で同じことを行います。

var funcs = []

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

let変数ブロックのスコープを作成します。ブロックは中括弧で示しますが、forループの場合、初期化変数はi、この場合、中括弧で宣言されていると見なされます。)


1
この答えを読むまで、この概念を理解するのに苦労しました。それは非常に重要なポイントに触れます–の値はiグローバルスコープに設定されています。場合forループが終了すると実行中のグローバル値はi、その関数が(発言権を使用してアレイに呼び出されるたびにそのため、今3であるfuncs[j])、iその関数内のグローバル参照しているi(3である)変数。
Modermo 2017

13

さまざまなソリューションを読んだ後、それらのソリューションが機能する理由は、スコープチェーンの概念に依存するためであることを付け加えておきます。これは、JavaScriptが実行中に変数を解決する方法です。

  • 各関数定義は、varおよびによって宣言されたすべてのローカル変数で構成されるスコープを形成しますarguments
  • 内部関数が別の(外部)関数内で定義されている場合、これはチェーンを形成し、実行中に使用されます
  • 関数が実行されると、ランタイムはスコープチェーンを検索して変数を評価します。チェーンの特定のポイントで変数が見つかると、検索を停止して使用します。それ以外の場合は、に属するグローバルスコープに到達するまで継続しwindowます。

最初のコードでは:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

ときにfuncs実行されます、スコープチェーンになりますfunction inner -> global。変数iはで見つからないfunction innervar引数として使用したり、引数として渡したりしない)のでi、グローバルスコープであるの値が最終的に見つかるまで、検索が続行されますwindow.i

それを外部関数でラップすることにより、hartoのようにヘルパー関数を明示的に定義するか、Bjornのように無名関数を使用します。

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

ときにfuncs実行されます、今スコープチェーンになりますfunction inner -> function outer。この時間iは、forループで3回実行される外部関数のスコープ内にあります。それぞれの時間は値iが正しくバインドされています。window.i内部実行時の値は使用しません。

詳細について は、こちらをご覧ください。ここ
には、ループ内にクロージャを作成する際の一般的な間違いと、クロージャが必要な理由とパフォーマンスの考慮事項が含まれています。


このコードサンプルを実際に作成することはめったにありませんが、基本を理解する良い例となると思います。スコープとそれらがどのように連鎖するかを考えたら、なぜ他の「モダン」な方法がArray.prototype.forEach(function callback(el) {})自然に機能するのかを理解するのがより明確になりますforEach。したがって、コールバックで定義されたすべての内部関数は正しいel値を使用できるようになります
wpding

13

ES6の新機能により、ブロックレベルのスコープが管理されます。

var funcs = [];
for (let i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (let j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

OPの質問のコードはのlet代わりに置き換えられvarます。


const同じ結果が得られ、変数の値が変化しない場合に使用する必要があります。ただし、constforループの初期化子の内部での使用は、Firefoxでは正しく実装されておらず、まだ修正されていません。ブロック内で宣言されるのではなく、ブロック外で宣言されるため、変数が再宣言され、エラーが発生します。let初期化子の内部での使用はFirefoxで正しく実装されているので、そこで心配する必要はありません。

10

forEachローカル変数の使用を(再)回避するための関数の使用をまだ誰も提案していないことに驚いています。実際、私はfor(var i ...)この理由でもう使用していません。

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

// forEachマップの代わりに使用するように編集されました。


3
.forEach()実際に何もマッピングしていない場合は、はるかに優れたオプションです。投稿する7か月前にダリルが提案したので、驚くべきことは何もありません。
JLRishe 2015年

この質問は配列のループについてではありません
jherax '27 / 10/27

さて、彼は関数の配列を作成したいと考えています。この例は、グローバル変数を使用せずにそれを行う方法を示しています。
Christian Landgren 2015

9

この質問は本当にJavaScriptの歴史を示しています!これで、矢印関数を使用したブロックスコープを回避し、Objectメソッドを使用してDOMノードから直接ループを処理できます。

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>


8

元の例が機能しなかった理由は、ループで作成したすべてのクロージャーが同じフレームを参照したためです。実際には、1つのi変数のみを持つ1つのオブジェクトに3つのメソッドがあります。それらはすべて同じ値を出力しました。


8

まず、このコードの何が問題になっているのかを理解してください。

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

ここで、funcs[]配列が初期化され、iインクリメントされると、funcs配列は初期化され、func配列のサイズは3になりi = 3,ます。これで、funcs[j]()が呼び出されたときに、iすでに3にインクリメントされている変数が再び使用されています。

これを解決するために、多くのオプションがあります。以下はそのうちの2つです。

  1. で初期化iするletか、新しい変数indexで初期化して、letと等しくすることができますi。したがって、呼び出しが行わindexれると、が使用され、そのスコープは初期化後に終了します。そして呼び出しのために、index再び初期化されます:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
  2. 他のオプションtempFuncは、実際の関数を返すaを導入することです:

    var funcs = [];
    function tempFunc(i){
        return function(){
            console.log("My value: " + i);
        };
    }
    for (var i = 0; i < 3; i++) {  
        funcs[i] = tempFunc(i);                                     
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }

8

クロージャー構造を使用してください。これにより、ループの追加が削減されます。あなたはそれを単一のforループで行うことができます:

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}

7

1つずつ宣言するvarと実際に何が起こるかを確認しますlet

Case1使用var

<script>
   var funcs = [];
   for (var i = 0; i < 3; i++) {
     funcs[i] = function () {
        debugger;
        console.log("My value: " + i);
     };
   }
   console.log(funcs);
</script>

F12キーを押してChrome コンソールウィンドウを開き、ページを更新します。配列内の3つの関数ごとに消費します。.Expandというプロパティが表示されます。と呼ばれる1つの配列オブジェクトが表示されます。値3を持つオブジェクトに宣言されたプロパティが見つかります。[[Scopes]]"Global"'i'

ここに画像の説明を入力してください

ここに画像の説明を入力してください

結論:

  1. 'var'関数の外部で変数を宣言すると、グローバル変数になります(入力するiwindow.i、コンソールウィンドウで確認できます。3が返されます)。
  2. 宣言した異常な関数は、関数を呼び出さない限り、関数内の値を呼び出してチェックしません。
  3. 関数を呼び出すと、オブジェクトconsole.log("My value: " + i)から値を取得しGlobalて結果を表示します。

CASE2:letを使用する

今置き換える'var''let'

<script>
    var funcs = [];
    for (let i = 0; i < 3; i++) {
        funcs[i] = function () {
           debugger;
           console.log("My value: " + i);
        };
    }
    console.log(funcs);
</script>

同じことを行って、スコープに移動します。これで、2つのオブジェクト"Block"とが表示されます"Global"Blockオブジェクトを展開すると、「i」がそこで定義されていることがわかります。奇妙なのは、すべての関数で、値if i(0、1、2)が異なるということです。

ここに画像の説明を入力してください

結論:

'let'関数の外側でもループの内側でも変数を宣言すると、この変数はグローバル変数にならずBlock、同じ関数でのみ使用できるレベル変数になります。そのため、i異なる値を取得しています関数を呼び出すときの各関数。

接近性の詳細については、素晴らしい動画チュートリアルhttps://youtu.be/71AtaJpJHw0をご覧ください。


4

query-js(*)などのデータのリストに宣言モジュールを使用できます。これらの状況では、私は個人的に宣言的アプローチはそれほど驚くべきことではないと感じています

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

次に、2番目のループを使用して期待される結果を得るか、または

funcs.iterate(function(f){ f(); });

(*)私はquery-jsの作成者であり、そのため使用する傾向があるので、宣言的アプローチのためだけに私のライブラリーの推奨として私の言葉を使用しないでください:)


1
反対票の説明が欲しいです。コードは目の前の問題を解決します。コードを潜在的に改善する方法を知ることは価値があります
Rune FS

1
なにQuery.range(0,3)?これはこの質問のタグの一部ではありません。また、サードパーティのライブラリを使用している場合は、ドキュメントのリンクを提供できます。
jherax、2015年

1
@jheraxこれらはもちろん明らかな改善です。コメントをありがとう。私はすでにリンクがあったと誓ったかもしれません。投稿がかなり無意味だったと思います:)。私が自分のライブラリの使用を押し付けようとしたのではなく、宣言的なアイデアを押し付けようとしたのではなく、それを除外するという私の最初の考えがありました。しかし、後の見通しでは、リンクがそこにあるべきだと私は完全に同意します
ルーンFS

4

forEachは擬似範囲を作成する独自のクロージャーを持つ関数を使用することを好みます:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

for (var j = 0; j < 3; j++) {
    funcs[j](); // 0, 1, 2
}

それは他の言語の範囲よりも醜く見えますが、IMHOは他のソリューションほど怪しいものではありません。


何を好む?これは、他の回答に対するコメントのようです。実際の質問にはまったく対応していません(後で呼び出される関数を割り当てていないため)。
クエンティン2015

これは、前述の問題と
完全に

今では、これは受け入れられた回答と大きく異なるようには見えません。
クエンティン2015

いいえ。受け入れられた回答では「いくつかの配列」を使用することをお勧めしますが、回答の範囲を扱いますが、それはまったく異なるものであり、残念ながらjsでは適切な解決策がないため、私の回答は解決しようとしています問題を適切かつ実践的な方法で
Rax Wunter

@クエンティン私はマイナスする前に解決策を調査す​​ることをお勧めします
Rax Wunter

4

さらに別の解決策:別のループを作成する代わりthisに、をreturn関数にバインドするだけです。

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

結合することによって、これを、問題を解決にも。


3

多くの解決策は正しいように見えますが、Curryingこれは、このような状況での関数型プログラミング設計パターンであると呼ばれていることについては触れていません。ブラウザによっては、バインドより3〜10倍高速です。

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

function curryShowValue(i) {
  return function showValue() {
    console.log("My value: " + i);
  }
}

さまざまなブラウザでのパフォーマンスの向上をご覧ください。


@TinyGiant関数が返される例は、パフォーマンスのために最適化されています。私は、すべてのJavaScriptブロガーのように、矢印関数のバンドワゴンにジャンプすることはしません。見た目はすっきりしていますが、事前定義された関数を使用するのではなく、インラインで関数を記述できます。これは、暑い場所では自明ではないトラップになることがあります。もう1つの問題は、不要なバインディングを実行してラッピングクロージャを作成しているため、それらが単なる構文上の砂糖ではないことです。
Pawel 2017

2
将来の読者への警告:この回答は、カリー化という用語を誤って適用しています。「カレーとは、複数の引数を取る関数を、引数の一部となる一連の関数に分解することです。」。このコードは何もしません。ここで行ったのは、受け入れられた回答からコードを取得し、いくつかを移動し、スタイルを変更して少し名前を付け、それをカレーと呼びますが、これは断固としてそうではありません。

3

コードは次の理由で機能しません。

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

問題は、i関数が呼び出されたときの変数の値は何ですか?最初のループはの条件で作成されるため、i < 3条件がfalseになるとすぐに停止するため、ですi = 3

関数が作成されたときに、そのコードは実行されず、後で使用するためにのみ保存されることを理解する必要があります。そして、それらが後で呼び出されると、インタープリターがそれらを実行して、「現在の値はi何ですか?」と尋ねます。

したがって、目的は最初にito関数の値を保存し、その後に関数をに保存することfuncsです。これは、たとえば次の方法で行うことができます。

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function(x) {            // and store them in funcs
        console.log("My value: " + x); // each should log its value.
    }.bind(null, i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

このように、各関数は独自の変数をx持ち、これxi各反復のの値に設定します。

これは、この問題を解決するための複数の方法の1つにすぎません。


3
var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function(param) {          // and store them in funcs
    console.log("My value: " + param); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j](j);                      // and now let's run each one to see with j
}

3

varの代わりにlet(blocked-scope)を使用してください。

var funcs = [];
for (let i = 0; i < 3; i++) {      
  funcs[i] = function() {          
    console.log("My value: " + i); 
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      
}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.