Frage JavaScript-Verschluss in Schleifen - einfaches praktisches Beispiel


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
}

Es gibt folgendes aus:

Mein Wert: 3
  Mein Wert: 3
  Mein Wert: 3

Während ich es gerne ausgeben würde:

Mein Wert: 0
  Mein Wert: 1
  Mein Wert: 2


Das gleiche Problem tritt auf, wenn die Verzögerung beim Ausführen der Funktion durch Verwendung von Ereignis-Listenern verursacht wird:

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

... oder asynchroner Code, z.B. Versprechungen verwenden:

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

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

Was ist die Lösung für dieses grundlegende Problem?


2317
2018-04-15 06:06


Ursprung


Antworten:


Nun, das Problem ist, dass die Variable i, innerhalb jeder Ihrer anonymen Funktionen, ist an die gleiche Variable außerhalb der Funktion gebunden.

Klassische Lösung: Verschlüsse

Was Sie tun möchten, ist, die Variable innerhalb jeder Funktion an einen separaten, unveränderlichen Wert außerhalb der Funktion zu binden:

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++) {
    funcs[j]();                        // and now let's run each one to see
}

Da es keinen JavaScript-Gültigkeitsbereich für JavaScript gibt - indem Sie die Funktionserstellung in eine neue Funktion einfügen, stellen Sie sicher, dass der Wert von "i" wie vorgesehen beibehalten wird.


Lösung 2015: forEach

Mit der relativ weit verbreiteten Verfügbarkeit der Array.prototype.forEach Funktion (im Jahr 2015), ist es erwähnenswert, dass in diesen Situationen Iteration in erster Linie über eine Reihe von Werten, .forEach() bietet einen sauberen, natürlichen Weg, um für jede Iteration einen eindeutigen Abschluss zu erhalten. Angenommen, Sie haben ein Array mit Werten (DOM-Referenzen, Objekte usw.) und das Problem, Callbacks einzurichten, die für jedes Element spezifisch sind, können Sie folgendermaßen ausführen:

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

Die Idee ist, dass jeder Aufruf der Callback - Funktion, die mit der .forEach Schleife wird ihre eigene Schließung sein. Der Parameter, der an diesen Handler übergeben wird, ist das für diesen bestimmten Schritt der Iteration spezifische Array-Element. Wenn es in einem asynchronen Rückruf verwendet wird, kollidiert es nicht mit irgendeinem der anderen Rückrufe, die in anderen Schritten der Iteration eingerichtet wurden.

Wenn Sie zufällig in jQuery arbeiten, wird die $.each() Funktion gibt Ihnen eine ähnliche Fähigkeit.


ES6-Lösung: let

ECMAScript 6 (ES6), die neueste Version von JavaScript, wird nun in vielen Evergreen-Browsern und Backend-Systemen implementiert. Es gibt auch transpilers wie Babel Dadurch wird ES6 in ES5 konvertiert, um die Verwendung neuer Funktionen auf älteren Systemen zu ermöglichen.

ES6 führt neue ein let und const Schlüsselwörter, die anders als var-basierte Variablen. Zum Beispiel in einer Schleife mit a letIndex, jede Iteration durch die Schleife hat einen neuen Wert von i Jeder Wert wird innerhalb der Schleife definiert, sodass der Code wie erwartet funktioniert. Es gibt viele Ressourcen, aber ich würde empfehlen 2ality's Block-Scoping Beitrag als eine große Informationsquelle.

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

Beachten Sie jedoch, dass IE9-IE11 und Edge vor Edge 14 unterstützt werden let aber die oben genannten falsch (sie erstellen keine neue i jedes Mal, also würden alle oben genannten Funktionen 3 protokollieren, so wie sie es tun würden, wenn wir es benutzen würden var). Edge 14 bringt es endlich richtig.


1795
2018-04-15 06:18



Versuchen:

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]();
}

Bearbeiten (2014):

Persönlich denke ich @ Austs neuere Antwort über die Verwendung .bind ist der beste Weg, um so etwas jetzt zu tun. Es gibt auch Lo-Dash / Unterstriche _.partial wenn du es nicht brauchst oder willst bindist es thisArg.


340
2018-04-15 06:10



Ein anderer Weg, der noch nicht erwähnt wurde, ist der Gebrauch von 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]();
}

AKTUALISIEREN

Wie von @squint und @mekdev gezeigt, erhalten Sie eine bessere Leistung, indem Sie zuerst die Funktion außerhalb der Schleife erstellen und dann die Ergebnisse innerhalb der Schleife binden.

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]();
}


310
2017-10-11 16:41



Mit einem Sofort aufgerufener Funktionsausdruck, die einfachste und lesbarste Art, eine Indexvariable zu umschließen:

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);

}

Dies sendet den Iterator i in die anonyme Funktion, die wir als definieren index. Dies schafft eine Schließung, wo die Variable i wird für die spätere Verwendung in jeder asynchronen Funktionalität innerhalb des IIFE gespeichert.


234
2017-10-11 18:23



Etwas spät zur Party, aber ich erkundete dieses Problem heute und bemerkte, dass viele der Antworten nicht vollständig behandeln, wie Javascript Bereiche behandelt, was im Wesentlichen darauf hinausläuft.

Wie viele andere bereits erwähnten, besteht das Problem darin, dass die innere Funktion dasselbe referenziert i Variable. Also, warum erstellen wir nicht einfach eine neue lokale Variable für jede Iteration, und haben stattdessen die innere Funktionsreferenz?

//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]();
}

Genau wie zuvor, wo jede innere Funktion den letzten zugewiesenen Wert ausgegeben hat i, jetzt gibt jede innere Funktion nur den letzten zugewiesenen Wert aus ilocal. Aber sollte nicht jede Iteration ihre eigene haben ilocal?

Es stellt sich heraus, das ist das Problem. Jede Iteration teilt denselben Bereich, sodass jede Iteration nach der ersten Iteration gerade überschrieben wird ilocal. Von MDN:

Wichtig: JavaScript hat keinen Blockbereich. Variablen, die mit einem Block eingeführt werden, sind auf die umschließende Funktion oder das Skript beschränkt, und die Auswirkungen der Einstellung bleiben über den Block hinaus bestehen. Mit anderen Worten, Blockanweisungen führen keinen Bereich ein. Obwohl "Standalone" -Bausteine ​​gültige Syntax sind, möchten Sie keine eigenständigen Blöcke in JavaScript verwenden, da sie nicht tun, was Sie denken, wenn sie denken, dass sie solche Blöcke in C oder Java machen.

Wiederholt für den Schwerpunkt:

JavaScript hat keinen Blockbereich. Variablen, die mit einem Block eingeführt werden, sind auf die enthaltende Funktion oder das Skript beschränkt

Wir können dies durch Überprüfung sehen ilocal bevor wir es in jeder Iteration deklarieren:

//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;
}

Dies ist genau der Grund, warum dieser Fehler so schwierig ist. Obwohl Sie eine Variable neu deklarieren, wird Javascript keinen Fehler verursachen, und JSLint wird nicht einmal eine Warnung werfen. Das ist auch der Grund, warum es am besten ist, Closures zu nutzen, was im Wesentlichen die Idee ist, dass innere Funktionen in Javascript Zugriff auf äußere Variablen haben, weil innere Bereiche äußere Bereiche "einschließen".

Closures

Dies bedeutet auch, dass innere Funktionen äußere Variablen "halten" und sie am Leben halten, selbst wenn die äußere Funktion zurückkehrt. Um dies zu nutzen, erstellen und rufen wir eine Wrapper-Funktion auf, um einen neuen Bereich zu deklarieren ilocalim neuen Bereich und gibt eine innere Funktion zurück, die verwendet ilocal (mehr Erklärung unten):

//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]();
}

Das Erstellen der inneren Funktion innerhalb einer Wrapperfunktion gibt der inneren Funktion eine private Umgebung, auf die nur sie zugreifen kann, eine "Schließung". Jedes Mal, wenn wir die Wrapper-Funktion aufrufen, erstellen wir eine neue innere Funktion mit einer eigenen separaten Umgebung, die sicherstellt, dass die ilocal Variablen kollidieren nicht und überschreiben sich gegenseitig. Ein paar kleine Optimierungen geben die endgültige Antwort, die viele andere SO-Benutzer gaben:

//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);
    };
}

Aktualisieren

Mit ES6 jetzt Mainstream, können wir jetzt das neue verwenden let Schlüsselwort zum Erstellen von Block-Variablen:

//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]();
}

Schau, wie einfach es jetzt ist! Für weitere Informationen siehe diese Antwort, auf dem meine Informationen basieren.


127
2018-04-10 09:57



Mit ES6 jetzt weitgehend unterstützt, hat sich die beste Antwort auf diese Frage geändert. ES6 bietet die let und const Schlüsselwörter für diesen genauen Umstand. Anstatt mit Verschlüssen herumzualbern, können wir einfach verwenden let So legen Sie eine Schleifenbereichsvariable fest:

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

val wird dann auf ein Objekt zeigen, das für diese bestimmte Schleife der Schleife spezifisch ist, und wird den richtigen Wert ohne die zusätzliche Schließnotation zurückgeben. Dies vereinfacht dieses Problem offensichtlich erheblich.

const ist ähnlich wie let mit der zusätzlichen Einschränkung, dass der Variablenname nach der ersten Zuweisung nicht zu einer neuen Referenz wiederhergestellt werden kann.

Browser-Unterstützung ist jetzt für diejenigen verfügbar, die auf die neuesten Versionen von Browsern abzielen. const/let werden derzeit im aktuellen Firefox, Safari, Edge und Chrome unterstützt. Es wird auch in Node unterstützt, und Sie können es überall verwenden, indem Sie Build-Tools wie Babel nutzen. Sie können hier ein funktionierendes Beispiel sehen: http://jsfiddle.net/ben336/rbU4t/2/

Dokumente hier:

Beachten Sie jedoch, dass IE9-IE11 und Edge vor Edge 14 unterstützt werden let aber die oben genannten falsch (sie erstellen keine neue i jedes Mal, also würden alle oben genannten Funktionen 3 protokollieren, so wie sie es tun würden, wenn wir es benutzen würden var). Edge 14 bringt es endlich richtig.


121
2018-05-21 03:04



Eine andere Art zu sagen ist, dass die i in Ihrer Funktion ist zum Zeitpunkt der Ausführung der Funktion gebunden, nicht der Zeitpunkt der Erstellung der Funktion.

Wenn Sie die Schließung erstellen, i ist eine Referenz auf die Variable, die im externen Bereich definiert ist, und nicht eine Kopie davon, wie sie beim Erstellen des Abschlusses erstellt wurde. Es wird zum Zeitpunkt der Ausführung ausgewertet.

Die meisten anderen Antworten bieten Möglichkeiten zum Umgehen, indem Sie eine andere Variable erstellen, die den Wert für Sie nicht ändert.

Ich dachte nur, ich würde eine Erklärung für Klarheit hinzufügen. Für eine Lösung, persönlich, würde ich mit Harto gehen, da es die selbsterklärende Art ist, es aus den Antworten hier zu tun. Jeder der Code funktioniert, aber ich würde für eine Schließung Fabrik über einen Stapel von Kommentaren zu schreiben, um zu erklären, warum ich eine neue Variable (Freddy und 1800) zu deklarieren oder seltsame eingebettete Schließung Syntax (Apphacker) haben.


77
2018-04-15 06:48



Was Sie verstehen müssen, ist der Umfang der Variablen in Javascript basiert auf der Funktion. Dies ist ein wichtiger Unterschied zu c #, in dem Sie einen Blockbereich haben, und nur das Kopieren der Variablen in den for wird funktionieren.

Wenn man es in eine Funktion einfügt, die die Rückgabe der Funktion wie Apphackers Antwort auswertet, wird es den Trick geben, da die Variable jetzt den Funktionsumfang hat.

Es gibt auch ein Schlüsselwort let anstelle von var, das die Verwendung der Blockbereichsregel zulässt. In diesem Fall würde das Definieren einer Variablen innerhalb des Forts den Trick darstellen. Allerdings ist das Schlüsselwort letkey aufgrund der Kompatibilität keine praktische Lösung.

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]();                        
}

61
2018-04-15 06:25



Hier ist eine weitere Variante der Technik, ähnlich wie bei Bjorns (Apphacker), mit der Sie den Variablenwert innerhalb der Funktion zuweisen können, anstatt ihn als Parameter zu übergeben, was manchmal klarer sein könnte:

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

Beachten Sie, dass die Technik, die Sie verwenden, die index Variable wird zu einer Art statischer Variable, die an die zurückgegebene Kopie der inneren Funktion gebunden ist. Das heißt, Änderungen an seinem Wert bleiben zwischen den Anrufen erhalten. Es kann sehr praktisch sein.


49
2017-08-07 08:45



Dies beschreibt den häufigen Fehler bei der Verwendung von Verschlüssen in JavaScript.

Eine Funktion definiert eine neue Umgebung

Erwägen:

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

Für jedes Mal makeCounter wird aufgerufen, {counter: 0} führt dazu, dass ein neues Objekt erstellt wird. Auch eine neue Kopie von obj  wird auch erstellt, um auf das neue Objekt zu verweisen. So, counter1 und counter2 sind unabhängig voneinander.

Verschlüsse in Schleifen

Die Verwendung eines Verschlusses in einer Schleife ist schwierig.

Erwägen:

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

Beachte das counters[0] und counters[1] sind nicht unabhängig. Tatsächlich arbeiten sie gleich obj!

Dies liegt daran, dass es nur eine Kopie von gibt obj über alle Iterationen der Schleife verteilt, vielleicht aus Leistungsgründen. Obwohl {counter: 0} Erstellt in jeder Iteration ein neues Objekt, die gleiche Kopie von obj wird nur mit einem aktualisiert werden Verweis auf das neueste Objekt.

Lösung ist die Verwendung einer anderen Hilfsfunktion:

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);
  }
}

Dies funktioniert, weil sowohl lokale Variablen im Funktionsumfang als auch Funktionsargumentvariablen zugewiesen werden neue Kopien bei der Einreise.

Für eine detaillierte Diskussion, sehen Sie bitte JavaScript-Fallgruben und Verwendung


45
2018-04-20 09:59