JavaScript Closure innerhalb von Schleifen – einfaches Praxisbeispiel
Lesezeit: 11 Minuten
Nickf
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]();
}
Es gibt dies aus:
Mein Wert: 3
Mein Wert: 3
Mein Wert: 3
Während ich möchte, dass es ausgibt:
Mein Wert: 0
Mein Wert: 1
Mein Wert: 2
Das gleiche Problem tritt auf, wenn die Verzögerung beim Ausführen 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);
});
}
// 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));
}
Es zeigt sich auch in for in und 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?
Du willst sicher nicht funcs ein Array sein, wenn Sie numerische Indizes verwenden? Nur ein Kopf hoch.
Im ES6ist eine triviale Lösung, die Variable zu deklarieren ich mit Lassendie auf den Schleifenkörper beschränkt ist.
– Thomas Nikodym
13. September 2016 um 20:34 Uhr
Björn
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 zur Verwendung .bind ist der beste Weg, so etwas jetzt zu tun. Es gibt auch Lo-Bindestriche/Unterstriche _.partial wenn Sie nicht brauchen oder wollen bind‘S thisArg.
jede Erklärung über die }(i)); ?
– aswzen
6. April 2018 um 1:32 Uhr
@aswzen Ich denke, es geht vorbei i als Argument index zur Funktion.
– Jetblau
26. Juli 2018 um 22:01 Uhr
es erstellt tatsächlich einen lokalen Variablenindex.
– Abhishek Singh
15. März 2019 um 15:17 Uhr
Funktionsausdruck sofort aufrufen, auch bekannt als IIFE. (i) ist das Argument für den anonymen Funktionsausdruck, der sofort aufgerufen wird, und der Index wird von i gesetzt.
– Eier
14. April 2020 um 5:51 Uhr
Das mache ich heutzutage auch, ich mag auch Lo-Bindestriche/Unterstriche _.partial
– Björn
8. Dezember 2014 um 5:18 Uhr
.bind() wird mit ECMAScript 6-Funktionen weitgehend veraltet sein. Außerdem erzeugt dies tatsächlich zwei Funktionen pro Iteration. Zuerst die anonyme, dann die generierte von .bind(). Besser wäre es dann, es außerhalb der Schleife zu erstellen .bind() es hinein.
– Benutzer1106925
28. Juni 2015 um 3:29 Uhr
@squint @mekdev – Ihr habt beide Recht. Mein erstes Beispiel wurde schnell geschrieben, um zu demonstrieren, wie bind wird genutzt. Ich habe ein weiteres Beispiel gemäß Ihren Vorschlägen hinzugefügt.
– Aust
29. Juni 2015 um 16:23 Uhr
Ich denke, anstatt Berechnungen über zwei O(n)-Schleifen zu verschwenden, tun Sie einfach for (var i = 0; i < 3; i++) { log.call(this, i); }
– Benutzer2290820
11. September 2015 um 12:14 Uhr
.bind() tut, was die akzeptierte Antwort vorschlägt PLUS spielt mit this.
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 definieren als index. Dadurch entsteht ein Abschluss, bei dem die Variable i wird für die spätere Verwendung in jeder asynchronen Funktionalität innerhalb des IIFE gespeichert.
Für eine bessere Lesbarkeit des Codes und um Verwirrung darüber zu vermeiden i was ist, würde ich den Funktionsparameter in umbenennen index.
– Kyle Falconer
10. Januar 2014 um 16:45 Uhr
Wie würden Sie diese Technik verwenden, um das Array zu definieren? Funktionen in der ursprünglichen Frage beschrieben?
– Nico
30. November 2014 um 13:17 Uhr
@Nico Auf die gleiche Weise wie in der ursprünglichen Frage gezeigt, außer dass Sie verwenden würden index anstatt i.
– JLRishe
31. März 2015 um 20:54 Uhr
@JLRishe var 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](); }
– Nico
1. April 2015 um 9:22 Uhr
@Nico Im speziellen Fall von OP iterieren sie nur über Zahlen, daher wäre dies kein guter Fall .forEach()aber oft, wenn man mit einem Array beginnt, forEach() ist eine gute Wahl, wie: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
– JLRishe
1. April 2015 um 10:05 Uhr
Gemeinschaft
Etwas spät zur Party, aber ich habe dieses Problem heute untersucht und festgestellt, dass viele der Antworten nicht vollständig darauf eingehen, wie Javascript Bereiche behandelt, worauf es im Wesentlichen hinausläuft.
Wie viele andere bereits erwähnt haben, besteht das Problem darin, dass die innere Funktion auf dasselbe verweist i Variable. Warum erstellen wir also nicht einfach bei jeder Iteration eine neue lokale Variable und lassen stattdessen die innere Funktion darauf 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 zuletzt zugewiesenen Wert ausgab ijetzt 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 sich denselben Bereich, sodass jede Iteration nach der ersten nur überschrieben wird ilocal. Von MDN:
Wichtig: JavaScript hat keinen Blockbereich. Variablen, die mit einem Block eingeführt werden, sind auf die enthaltende Funktion oder das enthaltende 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 “eigenständige” Blöcke eine gültige Syntax sind, sollten Sie keine eigenständigen Blöcke in JavaScript verwenden, da sie nicht das tun, was Sie denken, wenn Sie denken, dass sie so etwas wie solche Blöcke in C oder Java tun.
Wiederholt zur Betonung:
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 sehen, indem wir überprüfen 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;
}
Genau aus diesem Grund ist dieser Fehler so knifflig. Auch wenn Sie eine Variable neu deklarieren, gibt Javascript keinen Fehler aus und JSLint gibt nicht einmal eine Warnung aus. Dies ist auch der Grund, warum der beste Weg, dies zu lösen, darin besteht, Closures zu nutzen, was im Wesentlichen die Idee ist, dass in Javascript innere Funktionen Zugriff auf äußere Variablen haben, weil innere Bereiche äußere Bereiche “einschließen”.
Dies bedeutet auch, dass innere Funktionen äußere Variablen “festhalten” und am Leben erhalten, selbst wenn die äußere Funktion zurückkehrt. Um dies zu nutzen, erstellen und rufen wir eine Wrapper-Funktion auf, um lediglich einen neuen Geltungsbereich zu deklarieren ilocal im neuen Geltungsbereich und geben 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 Wrapper-Funktion gibt der inneren Funktion eine private Umgebung, auf die nur sie zugreifen kann, einen “Abschluss”. Daher erstellen wir jedes Mal, wenn wir die Wrapper-Funktion aufrufen, eine neue innere Funktion mit einer eigenen separaten Umgebung, um sicherzustellen, dass die ilocal Variablen kollidieren nicht und überschreiben sich gegenseitig. Ein paar kleinere Optimierungen geben die endgültige Antwort, die viele andere SO-Benutzer gegeben haben:
//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
Da ES6 jetzt Mainstream ist, können wir jetzt das Neue nutzen let Schlüsselwort zum Erstellen von blockbezogenen 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! Weitere Informationen finden Sie in dieser Antwort, auf der meine Informationen basieren.
Für eine bessere Lesbarkeit des Codes und um Verwirrung darüber zu vermeiden i was ist, würde ich den Funktionsparameter in umbenennen index.
– Kyle Falconer
10. Januar 2014 um 16:45 Uhr
Wie würden Sie diese Technik verwenden, um das Array zu definieren? Funktionen in der ursprünglichen Frage beschrieben?
– Nico
30. November 2014 um 13:17 Uhr
@Nico Auf die gleiche Weise wie in der ursprünglichen Frage gezeigt, außer dass Sie verwenden würden index anstatt i.
– JLRishe
31. März 2015 um 20:54 Uhr
@JLRishe var 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](); }
– Nico
1. April 2015 um 9:22 Uhr
@Nico Im speziellen Fall von OP iterieren sie nur über Zahlen, daher wäre dies kein guter Fall .forEach()aber oft, wenn man mit einem Array beginnt, forEach() ist eine gute Wahl, wie: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
– JLRishe
1. April 2015 um 10:05 Uhr
henser
Da ES6 jetzt weithin unterstützt wird, hat sich die beste Antwort auf diese Frage geändert. ES6 bietet die let und const Schlüsselwörter für genau diesen Umstand. Anstatt mit Verschlüssen herumzuspielen, können wir einfach verwenden let um eine Loop-Scope-Variable wie folgt zu setzen:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
val zeigt dann auf ein Objekt, das spezifisch für diese bestimmte Wendung der Schleife ist, und gibt den korrekten Wert ohne die zusätzliche Abschlussnotation zurück. Dies vereinfacht dieses Problem offensichtlich erheblich.
const ist ähnlich wie let mit der zusätzlichen Einschränkung, dass der Variablenname nach der anfänglichen Zuweisung nicht an eine neue Referenz gebunden werden kann.
Die Browserunterstützung ist jetzt für diejenigen da, die auf die neuesten Browserversionen abzielen. const/let werden derzeit in den neuesten Versionen von 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. Ein funktionierendes Beispiel sehen Sie hier: http://jsfiddle.net/ben336/rbU4t/2/
Beachten Sie jedoch, dass IE9-IE11 und Edge vor Edge 14 unterstützt werden let aber verstehe das obige falsch (sie erstellen keine neue i jedes Mal, also würden alle oben genannten Funktionen 3 protokollieren, wie sie es tun würden, wenn wir sie verwenden würden var). Edge 14 macht es endlich richtig.
Ab Juni ’16, Lassen wird in allen gängigen Browserversionen außer iOS Safari, Opera Mini und Safari 9 unterstützt. Evergreen-Browser unterstützen dies. Babel wird es korrekt transpilieren, um das erwartete Verhalten beizubehalten, ohne dass der Modus für hohe Kompatibilität eingeschaltet ist.
– Dan
22. Juni 2016 um 10:18 Uhr
@DanPantry ja, es wird Zeit für ein Update 🙂 Aktualisiert, um den aktuellen Stand der Dinge besser widerzuspiegeln, einschließlich Hinzufügen einer Erwähnung von const, Doc-Links und besserer Kompatibilitätsinformationen.
– Ben McCormick
27. Juni 2016 um 14:24 Uhr
Ist das nicht der Grund, warum wir babel verwenden, um unseren Code zu transpilieren, damit Browser, die ES6/7 nicht unterstützen, verstehen können, was vor sich geht?
– Pixel 67
19. März 2018 um 15:56 Uhr
9874000cookie-checkJavaScript Closure innerhalb von Schleifen – einfaches Praxisbeispielyes
Du willst sicher nicht
funcs
ein Array sein, wenn Sie numerische Indizes verwenden? Nur ein Kopf hoch.– DanMan
26. Juli 2013 um 11:12 Uhr
Das ist ein wirklich verwirrendes Problem. Dies Artikel hilft mir, es zu verstehen. Vielleicht hilft es auch anderen.
– Benutzer3199690
3. Mai 2014 um 15:38 Uhr
Eine weitere einfache und erklärte Lösung: 1) Verschachtelte Funktionen haben Zugriff auf den Geltungsbereich “über” ihnen; 2) ein Schließung Lösung… “Ein Abschluss ist eine Funktion, die Zugriff auf den übergeordneten Bereich hat, auch nachdem die übergeordnete Funktion geschlossen wurde”.
– Peter Krauß
17. Dezember 2014 um 1:22 Uhr
Verweisen Sie auf diesen Link für ein besseres Verständnis javascript.info/tutorial/advanced-functions
– Saurabh Ahuja
2. April 2015 um 12:01 Uhr
Im ES6ist eine triviale Lösung, die Variable zu deklarieren ich mit Lassendie auf den Schleifenkörper beschränkt ist.
– Thomas Nikodym
13. September 2016 um 20:34 Uhr