3254 Stimmen

JavaScript-Schließung in Schleifen - einfaches praktisches Beispiel

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

Sie gibt dies aus:

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

Ich hingegen möchte, dass es ausgegeben wird:

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


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

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>

oder asynchroner Code, z.B. mit 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));
}

Das zeigt sich auch in for in y for of Schleifen:

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

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

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

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

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

62 Stimmen

Sind Sie sicher, dass Sie nicht wollen, dass funcs ein Array zu sein, wenn Sie numerische Indizes verwenden? Nur eine Vorwarnung.

33 Stimmen

Das ist ein wirklich verwirrendes Problem. Diese Artikel hilft mir, ihn zu verstehen . Vielleicht hilft es ja auch anderen.

4 Stimmen

Eine weitere einfache und gut erklärte Lösung: 1) Verschachtelte Funktionen Zugriff auf den Bereich "über" ihnen haben ; 2) a Verschluss Lösung ... "Eine Schließung ist eine Funktion, die Zugriff auf den übergeordneten Bereich hat, auch nachdem die übergeordnete Funktion geschlossen wurde".

2402voto

harto Punkte 87633

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

ES6-Lösung: let

ECMAScript 6 (ES6) führt neue let y const Schlüsselwörter, die anders skaliert sind als var -basierten Variablen. Zum Beispiel, in einer Schleife mit einer let -basierten Index, wird bei jedem Durchlauf der Schleife eine neue Variable i mit Schleifenbereich, so dass Ihr Code wie erwartet funktionieren würde. Es gibt viele Ressourcen, aber ich würde empfehlen 2alitys Block-Scoping-Post als eine großartige 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 Folgendes unterstützen let aber die obigen Angaben sind falsch (sie erstellen keine neue i jedes Mal, so dass alle obigen Funktionen 3 protokollieren würden, wie sie es tun würden, wenn wir var ). Edge 14 macht es endlich richtig.


ES5.1 Lösung: forEach

Mit der relativ breiten Verfügbarkeit des Array.prototype.forEach Funktion (im Jahr 2015), ist es erwähnenswert, dass in diesen Situationen, die Iteration vor allem über ein Array von Werten, .forEach() bietet eine saubere, natürliche Möglichkeit, einen eindeutigen Abschluss für jede Iteration zu erhalten. Das heißt, vorausgesetzt, Sie haben eine Art von Array mit Werten (DOM-Referenzen, Objekte, was auch immer), und das Problem der Einrichtung von Rückrufen spezifisch für jedes Element, können Sie dies tun:

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 ein eigener Abschluss sein. Der Parameter, der an diesen Handler übergeben wird, ist das Array-Element, das für diesen bestimmten Schritt der Iteration spezifisch ist. Wenn es in einem asynchronen Callback verwendet wird, kollidiert es nicht mit anderen Callbacks, die in anderen Schritten der Iteration eingerichtet wurden.

Wenn Sie mit jQuery arbeiten, können Sie die $.each() Funktion bietet Ihnen eine ähnliche Möglichkeit.


Klassische Lösung: Verschlüsse

Sie wollen die Variable innerhalb jeder Funktion an einen separaten, unveränderlichen Wert außerhalb der Funktion 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++) {
  // and now let's run each one to see
  funcs[j]();
}

Da es in JavaScript keinen Blockbereich gibt, sondern nur einen Funktionsbereich, können Sie durch das Einschließen der Funktionserstellung in eine neue Funktion sicherstellen, dass der Wert von "i" so bleibt, wie Sie es beabsichtigt haben.

423voto

Bjorn Punkte 64323

Versuchen Sie es:

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, dass @Aust's neuere Antwort über die Verwendung von .bind ist der beste Weg, um diese Art von Dingen zu tun. Es gibt auch lo-dash/underscore's _.partial wenn Sie sich nicht mit den folgenden Dingen herumschlagen müssen oder wollen bind 's thisArg .

392voto

Aust Punkte 11232

Eine weitere Möglichkeit, die noch nicht erwähnt wurde, ist die Verwendung 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]();
}

UPDATE

Wie von @squint und @mekdev dargelegt, erzielen Sie eine bessere Leistung, wenn Sie die Funktion zunächst 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]();
}

295voto

neurosnap Punkte 5418

Die Verwendung eines Unmittelbar ausgelöster Funktionsausdruck ist 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 wie folgt definieren index . Dadurch wird ein Abschluss geschaffen, bei dem die Variable i wird zur späteren Verwendung in asynchronen Funktionen innerhalb des IIFE gespeichert.

184voto

woojoo666 Punkte 7453

Bit spät zu der Partei, aber ich war dieses Problem heute erkunden und bemerkte, dass viele der Antworten nicht vollständig Adresse, wie Javascript behandelt Bereiche, die im Wesentlichen, was dies läuft darauf hinaus, ist.

Wie viele andere bereits erwähnt haben, besteht das Problem darin, dass die innere Funktion auf die gleiche i variabel. Warum erstellen wir also nicht einfach bei jeder Iteration eine neue lokale Variable und lassen die innere Funktion stattdessen auf diese verweisen?

//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 Wert ausgab, der der i gibt nun jede innere Funktion nur noch den letzten Wert aus, der der ilocal . Aber sollte nicht jede Iteration ihre eigene haben? ilocal ?

Wie sich herausstellt, ist das das Problem. Jede Iteration teilt den gleichen Bereich, so dass jede Iteration nach der ersten nur überschreiben ilocal . Von MDN :

Wichtig: JavaScript hat keinen Blockbereich. Variablen, die mit einem Block eingeführt werden, sind auf die enthaltende Funktion oder das Skript beschränkt, und die Auswirkungen ihrer Einstellung bleiben über den Block selbst hinaus bestehen. Mit anderen Worten: Blockanweisungen führen keinen Geltungsbereich ein. Obwohl "Standalone"-Blöcke eine gültige Syntax sind, sollten Sie keine Standalone-Blöcke in JavaScript verwenden, da sie nicht das tun, was Sie denken, wenn Sie glauben, dass sie etwas Ähnliches wie solche Blöcke in C oder Java tun.

Zur Betonung wiederholt:

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

Dies können wir sehen, indem wir prüfen ilocal bevor wir sie 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;
}

Das ist genau der Grund, warum dieser Fehler so heikel ist. Auch wenn Sie eine Variable neu deklarieren, wird Javascript keinen Fehler ausgeben, und JSLint wird nicht einmal eine Warnung ausgeben. Dies ist auch der Grund, warum der beste Weg, dies zu lösen ist, um die Vorteile von Schließungen, die im Wesentlichen die Idee, dass in Javascript, innere Funktionen haben Zugriff auf äußere Variablen, weil innere Bereiche "umschließen" äußeren Bereichen.

Closures

Das bedeutet auch, dass innere Funktionen äußere Variablen "festhalten" und sie am Leben erhalten, auch wenn die äußere Funktion zurückkehrt. Um dies zu nutzen, erstellen wir eine Wrapper-Funktion und rufen sie auf, um einen neuen Bereich zu erstellen, deklarieren ilocal im neuen Bereich und geben eine innere Funktion zurück, die ilocal (weitere 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]();
}

Durch die Erstellung der inneren Funktion innerhalb einer Wrapper-Funktion erhält die innere Funktion eine private Umgebung, auf die nur sie zugreifen kann, eine "Closure". Jedes Mal, wenn wir die Wrapper-Funktion aufrufen, erstellen wir also eine neue innere Funktion mit einer eigenen, separaten Umgebung, die sicherstellt, dass die ilocal Variablen nicht miteinander kollidieren und sich gegenseitig überschreiben. Ein paar kleinere Optimierungen ergeben 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);
    };
}

Update

Da ES6 nun zum Mainstream geworden ist, können wir jetzt die neue let Schlüsselwort, um Block-Scoped-Variablen zu erstellen:

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

Sehen Sie, wie einfach es jetzt ist! Für mehr Informationen siehe diese Antwort auf die sich meine Informationen stützen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X