Vraag JavaScript-sluiting binnen lussen - eenvoudig praktisch voorbeeld


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
}

Het geeft dit weer:

Mijn waarde: 3
  Mijn waarde: 3
  Mijn waarde: 3

Terwijl ik zou willen dat het zou produceren:

Mijn waarde: 0
  Mijn waarde: 1
  Mijn waarde: 2


Hetzelfde probleem treedt op wanneer de vertraging in het uitvoeren van de functie wordt veroorzaakt door gebeurtenislisteners:

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>

... of asynchrone code, b.v. met behulp van beloften:

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

Wat is de oplossing voor dit basisprobleem?


2317
2018-04-15 06:06


oorsprong


antwoorden:


Nou, het probleem is dat de variabele i, binnen elk van uw anonieme functies, is gebonden aan dezelfde variabele buiten de functie.

Klassieke oplossing: sluitingen

Wat u wilt doen is de variabele binnen elke functie binden aan een afzonderlijke, onveranderlijke waarde buiten de functie:

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
}

Aangezien er geen JavaScript is in JavaScript - alleen functiebereik - door de functie aan te maken in een nieuwe functie, zorgt u ervoor dat de waarde van "i" blijft zoals u van plan was.


Oplossing van 2015: voor elk

Met de relatief wijdverbreide beschikbaarheid van de Array.prototype.forEach functie (in 2015), is het de moeite waard om op te merken dat in die situaties waarbij iteratie primeert op een reeks waarden, .forEach() biedt een schone, natuurlijke manier om een ​​duidelijke afsluiting voor elke iteratie te krijgen. Dat wil zeggen, ervan uitgaande dat je een soort array hebt met waarden (DOM-verwijzingen, objecten, wat dan ook), en het probleem ontstaat met het instellen van callbacks die specifiek zijn voor elk element, kun je dit doen:

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

Het idee is dat elke aanroep van de callback-functie die wordt gebruikt met de .forEach loop zal zijn eigen afsluiting zijn. De parameter die aan die handler wordt doorgegeven, is het arrayelement dat specifiek is voor die specifieke stap van de iteratie. Als het in een asynchrone callback wordt gebruikt, zal het niet botsen met een van de andere callbacks die in andere stappen van de iteratie zijn vastgesteld.

Als je toevallig in jQuery werkt, de $.each() functie geeft u een vergelijkbare mogelijkheid.


ES6-oplossing: let

ECMAScript 6 (ES6), de nieuwste versie van JavaScript, wordt nu geïmplementeerd in veel groenblijvende browsers en backend-systemen. Er zijn ook transpilers zoals Babel die ES6 naar ES5 zal converteren om gebruik van nieuwe functies op oudere systemen mogelijk te maken.

ES6 introduceert nieuw let en const sleutelwoorden die anders zijn ingekaderd dan varop basis van variabelen. Bijvoorbeeld in een lus met een let-gebaseerde index, elke iteratie door de lus zal een nieuwe waarde hebben van i waarbij elke waarde binnen de lus wordt geschaald, zodat uw code zou werken zoals u verwacht. Er zijn veel middelen, maar ik zou het aanraden 2ality's block-scoping post als een grote bron van informatie.

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

Pas echter op dat IE9-IE11 en Edge voorafgaand aan Edge 14-ondersteuning let maar krijg het bovenstaande verkeerd (ze creëren geen nieuw i elke keer, dus alle functies hierboven zouden 3 loggen zoals ze zouden doen als we ze zouden gebruiken var). Rand 14 krijgt het eindelijk goed.


1795
2018-04-15 06:18



Proberen:

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

Bewerk (2014):

Persoonlijk denk ik dat @ Aust's meer recent antwoord over het gebruik .bind is de beste manier om dit soort dingen nu te doen. Er zijn ook lo-dash / underscore's _.partial wanneer je niet nodig hebt of wilt rommelen met bind's thisArg.


340
2018-04-15 06:10



Een andere manier die nog niet is genoemd, is het gebruik van 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]();
}

BIJWERKEN

Zoals opgemerkt door @squint en @mekdev, krijgt u betere prestaties door eerst de functie buiten de lus te maken en vervolgens de resultaten binnen de lus te 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



Gebruik een Onmiddellijk-opgevraagde functie-expressie, de eenvoudigste en meest leesbare manier om een ​​indexvariabele te omsluiten:

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

}

Dit verzendt de iterator i naar de anonieme functie waarvan we definiëren als index. Dit creëert een afsluiting, waarbij de variabele i wordt opgeslagen voor later gebruik in asynchrone functionaliteit binnen de IIFE.


234
2017-10-11 18:23



Beetje laat op het feest, maar ik was dit probleem vandaag aan het verkennen en merkte dat veel van de antwoorden niet volledig ingaan op hoe Javascript scopes behandelt, wat in wezen is waar dit op neer komt.

Zoals vele anderen al zeiden, is het probleem dat de innerlijke functie naar hetzelfde verwijst i variabel. Dus waarom creëren we niet zomaar een nieuwe lokale variabele per iteratie en hebben we de innerlijke functie daarnaar verwijzen?

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

Net als voorheen, waarbij elke interne functie de laatste waarde heeft toegewezen waaraan is toegewezen i, nu voert elke interne functie gewoon de laatste waarde uit die is toegewezen aan ilocal. Maar zou niet elke iteratie zijn eigen moeten hebben ilocal?

Blijkbaar is dat het probleem. Elke iteratie deelt hetzelfde bereik, dus elke iteratie na de eerste overschrijft gewoon ilocal. Van MDN:

Belangrijk: JavaScript heeft geen blokbereik. Variabelen die met een blok worden geïntroduceerd, worden naar de bevattende functie of script geschaald en de effecten van het instellen ervan blijven bestaan ​​buiten het blok zelf. Met andere woorden, blokinstructies introduceren geen scope. Hoewel "zelfstandige" blokken een geldige syntaxis zijn, wilt u geen zelfstandige blokken in JavaScript gebruiken, omdat ze niet doen wat u denkt dat ze doen, als u denkt dat ze in C of Java zoiets als dergelijke blokken doen.

Herhaalde voor de nadruk:

JavaScript heeft geen block scope. Variabelen die met een blok worden geïntroduceerd, worden doorgevoerd naar de bevattende functie of het script

We kunnen dit zien door te controleren ilocal voordat we het in elke iteratie verklaren:

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

Dit is precies waarom deze bug zo lastig is. Hoewel u een variabele opnieuw declareert, zal JavaScript geen fout genereren en zal JSLint zelfs geen waarschuwing geven. Dit is ook de reden waarom de beste manier om dit op te lossen is om gebruik te maken van sluitingen, wat in essentie het idee is dat innerlijke functies in Javascript toegang hebben tot buitenvariabelen omdat binnenste scopes outer scopes "omhullen".

Closures

Dit betekent ook dat interne functies buitenste variabelen "vasthouden" en ze in leven houden, zelfs als de uiterlijke functie terugkeert. Om dit te gebruiken, creëren en noemen we een wrapper-functie puur om een ​​nieuwe scope te maken, verklaren ilocalin de nieuwe scope, en een innerlijke functie teruggeven die gebruikt ilocal (meer uitleg hieronder):

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

Het creëren van de innerlijke functie binnen een wrapperfunctie geeft de innerlijke functie een privéomgeving waartoe alleen het toegang heeft, een "sluiting". Dus elke keer dat we de wrapper-functie aanroepen, creëren we een nieuwe innerlijke functie met zijn eigen aparte omgeving, zodat het ilocal variabelen botsen niet en overschrijven elkaar. Een paar kleine optimalisaties geven het uiteindelijke antwoord dat veel andere SO-gebruikers gaven:

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

Bijwerken

Nu ES6 mainstream is, kunnen we nu het nieuwe gebruiken let sleutelwoord om variabelen met block-scoped te maken:

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

Kijk hoe gemakkelijk het nu is! Zie voor meer informatie dit antwoord, waar mijn info op is gebaseerd.


127
2018-04-10 09:57



Nu ES6 nu breed wordt ondersteund, is het beste antwoord op deze vraag veranderd. ES6 biedt de let en const sleutelwoorden voor deze exacte omstandigheid. In plaats van te rommelen met sluitingen, kunnen we het gewoon gebruiken let om een ​​loop scope-variabele als deze in te stellen:

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

val zal dan naar een object wijzen dat specifiek is voor die specifieke draaiing van de lus, en zal de correcte waarde retourneren zonder de extra sluitingsnotatie. Dit vereenvoudigt duidelijk dit probleem aanzienlijk.

const is gelijkaardig aan let met de extra beperking dat de variabelenaam na initiële toewijzing niet kan worden terugverwezen naar een nieuwe referentie.

Browserondersteuning is nu beschikbaar voor gebruikers die de nieuwste versies van browsers targeten. const/let worden momenteel ondersteund in de nieuwste Firefox, Safari, Edge en Chrome. Het wordt ook ondersteund in Node en je kunt het overal gebruiken door te profiteren van bouwtools zoals Babel. U kunt hier een werkvoorbeeld zien: http://jsfiddle.net/ben336/rbU4t/2/

Documenten hier:

Pas echter op dat IE9-IE11 en Edge voorafgaand aan Edge 14-ondersteuning let maar krijg het bovenstaande verkeerd (ze creëren geen nieuw i elke keer, dus alle functies hierboven zouden 3 loggen zoals ze zouden doen als we ze zouden gebruiken var). Rand 14 krijgt het eindelijk goed.


121
2018-05-21 03:04



Een andere manier om het te zeggen is dat het i in uw functie wordt gebonden op het moment van uitvoering van de functie, niet op het moment waarop de functie wordt gecreëerd.

Wanneer je de sluiting maakt, i is een verwijzing naar de variabele die is gedefinieerd in de externe scope, niet een kopie van de variabele zoals deze was toen u de afsluiting maakte. Het wordt geëvalueerd op het moment van uitvoering.

De meeste andere antwoorden bieden manieren om te werken door een andere variabele te maken die de waarde voor u niet zal veranderen.

Ik dacht gewoon dat ik een verklaring voor de duidelijkheid zou toevoegen. Voor een oplossing zou ik persoonlijk met Harto's gaan, omdat dit de meest vanzelfsprekende manier is om dit te doen aan de hand van de antwoorden hier. Elke geposte code zal werken, maar ik zou kiezen voor een sluitingsfabriek boven het schrijven van een stapel opmerkingen om uit te leggen waarom ik een nieuwe variabele (Freddy en 1800's) aan het verklaren ben of een rare ingesloten sluitsyntax (apphacker) heb.


77
2018-04-15 06:48



Wat u moet begrijpen is de reikwijdte van de variabelen in javascript is gebaseerd op de functie. Dit is een belangrijk verschil dan zeggen c # waar je een block scope hebt, en alleen het kopiëren van de variabele naar een binnen de for zal werken.

Het inpakken in een functie die evalueert dat het retourneren van de functie, zoals het antwoord van apphacker, de truc zal doen, omdat de variabele nu het functiebereik heeft.

Er is ook een let-sleutelwoord in plaats van var, waarmee u de regel voor blokbereik kunt gebruiken. In dat geval zou het definiëren van een variabele in de for the truc de juiste oplossing zijn. Dat gezegd hebbende, het let-trefwoord is geen praktische oplossing vanwege de compatibiliteit.

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