Question Fermeture JavaScript à l'intérieur des boucles - exemple pratique simple


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
}

Il sort ceci:

Ma valeur: 3
  Ma valeur: 3
  Ma valeur: 3

Alors que j'aimerais le sortir:

Ma valeur: 0
  Ma valeur: 1
  Ma valeur: 2


Le même problème se produit lorsque le délai d'exécution de la fonction est dû à l'utilisation d'écouteurs d'événement:

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>

... ou un code asynchrone, par ex. en utilisant des promesses:

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

Quelle est la solution à ce problème de base?


2317
2018-04-15 06:06


origine


Réponses:


Eh bien, le problème est que la variable i, dans chacune de vos fonctions anonymes, est lié à la même variable en dehors de la fonction.

Solution classique: Fermetures

Ce que vous voulez faire est de lier la variable dans chaque fonction à une valeur distincte et immuable en dehors de la fonction:

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
}

Comme il n'y a pas de portée de bloc en JavaScript - uniquement la portée de la fonction - en enveloppant la création de la fonction dans une nouvelle fonction, vous vous assurez que la valeur de "i" reste comme vous l'avez voulu.


Solution 2015: pourChaque

Avec la disponibilité relativement répandue de Array.prototype.forEach fonction (en 2015), il vaut la peine de noter que dans les situations impliquant l'itération principalement sur un ensemble de valeurs, .forEach() fournit un moyen propre et naturel d'obtenir une fermeture distincte pour chaque itération. C'est-à-dire, en supposant que vous ayez une sorte de tableau contenant des valeurs (références DOM, objets, peu importe), et que le problème se pose de configurer des rappels spécifiques à chaque élément, vous pouvez le faire:

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

L'idée est que chaque invocation de la fonction de rappel utilisée avec le .forEach boucle sera sa propre fermeture. Le paramètre transmis à ce gestionnaire est l'élément de tableau spécifique à cette étape particulière de l'itération. Si elle est utilisée dans un rappel asynchrone, elle ne sera pas en conflit avec les autres rappels établis aux autres étapes de l'itération.

Si vous travaillez dans jQuery, le $.each() fonction vous donne une capacité similaire.


Solution ES6: let

ECMAScript 6 (ES6), la nouvelle version de JavaScript, commence à être implémentée dans de nombreux navigateurs à feuilles persistantes et systèmes dorsaux. Il y a aussi des transpileurs comme Babel cela va convertir ES6 en ES5 pour permettre l'utilisation de nouvelles fonctionnalités sur les anciens systèmes.

ES6 introduit de nouvelles let et const mots-clés qui sont définis différemment de varvariables basées sur Par exemple, dans une boucle avec un letbasé sur la boucle, chaque itération à travers la boucle aura une nouvelle valeur de i où chaque valeur est comprise dans la boucle, de sorte que votre code fonctionne comme prévu. Il y a beaucoup de ressources, mais je recommanderais Poste de délimitation de bloc de 2ality comme une grande source d'information.

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

Attention, cependant, que IE9-IE11 et Edge avant le support Edge 14 let mais obtenez le mauvais ci-dessus (ils ne créent pas une nouvelle i chaque fois, donc toutes les fonctions ci-dessus se connecteraient 3 comme ils le feraient si nous utilisions var). Edge 14 obtient finalement le droit.


1795
2018-04-15 06:18



Essayer:

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

modifier (2014):

Personnellement, je pense @ Aust réponse plus récente à propos de l'utilisation .bind est le meilleur moyen de faire ce genre de chose maintenant. Il y a aussi lo-dash / underscore _.partial quand vous n'avez pas besoin ou ne voulez pas jouer avec bindde thisArg.


340
2018-04-15 06:10



Une autre façon qui n'a pas encore été mentionnée est l'utilisation de 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]();
}

METTRE À JOUR

Comme l'indiquent @squint et @mekdev, vous obtenez de meilleures performances en créant d'abord la fonction en dehors de la boucle, puis en liant les résultats dans la boucle.

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



En utilisant un Expression de fonction invoquée immédiatement, la manière la plus simple et la plus lisible d'inclure une variable d'index:

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

}

Cela envoie l'itérateur i dans la fonction anonyme dont nous définissons index. Cela crée une fermeture, où la variable i est enregistré pour une utilisation ultérieure dans toute fonctionnalité asynchrone au sein de l'IIFE.


234
2017-10-11 18:23



Un peu en retard à la fête, mais j'explorais ce problème aujourd'hui et j'ai remarqué que beaucoup de réponses ne traitent pas complètement de la manière dont Javascript traite les oscilloscopes, ce qui est essentiellement ce à quoi cela se réduit.

Comme beaucoup d'autres l'ont mentionné, le problème est que la fonction interne se réfère à la même i variable. Alors pourquoi ne pas simplement créer une nouvelle variable locale à chaque itération, et faire référence à la fonction interne à la place?

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

Juste comme avant, où chaque fonction interne produit la dernière valeur assignée à i, maintenant chaque fonction interne sort juste la dernière valeur assignée à ilocal. Mais chaque itération ne devrait-elle pas avoir son propre ilocal?

Il s'avère que c'est le problème. Chaque itération partage la même portée, de sorte que chaque itération après la première est juste écrasante ilocal. De MDN:

Important: JavaScript n'a pas de portée de bloc. Les variables introduites avec un bloc sont étendues à la fonction ou au script conteneur, et les effets de leur définition persistent au-delà du bloc lui-même. En d'autres termes, les instructions de bloc n'introduisent pas de portée. Bien que les blocs "autonomes" soient une syntaxe valide, vous ne voulez pas utiliser de blocs autonomes en JavaScript, car ils ne font pas ce que vous pensez qu'ils font, si vous pensez qu'ils font quelque chose comme de tels blocs en C ou Java.

Réitéré pour souligner:

JavaScript n'a pas de portée de bloc. Les variables introduites avec un bloc sont étendues à la fonction ou au script conteneur

Nous pouvons voir cela en vérifiant ilocal avant de le déclarer dans chaque itération:

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

C'est exactement pourquoi ce bug est si difficile. Même si vous redéclarez une variable, Javascript ne jettera pas d'erreur, et JSLint ne lancera même pas d'avertissement. C'est aussi pourquoi la meilleure façon de résoudre cela est de tirer parti des fermetures, ce qui est essentiellement l'idée qu'en Javascript, les fonctions internes ont accès aux variables externes parce que les étendues internes "entourent" les étendues externes.

Closures

Cela signifie également que les fonctions internes "maintiennent" les variables externes et les maintiennent en vie, même si la fonction externe revient. Pour utiliser cela, nous créons et appelons une fonction wrapper uniquement pour créer une nouvelle portée, déclarons ilocaldans la nouvelle portée, et retourner une fonction interne qui utilise ilocal (plus d'explications ci-dessous):

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

Créer la fonction interne à l'intérieur d'une fonction wrapper donne à la fonction interne un environnement privé auquel elle seule peut accéder, une "fermeture". Ainsi, chaque fois que nous appelons la fonction wrapper, nous créons une nouvelle fonction interne avec son propre environnement séparé, en veillant à ce que le ilocal les variables n'entrent pas en collision et s'écrasent mutuellement. Quelques optimisations mineures donnent la réponse finale que beaucoup d'autres utilisateurs de SO ont donné:

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

Mettre à jour

Avec ES6 maintenant grand public, nous pouvons maintenant utiliser le nouveau let mot-clé pour créer des variables à portée de bloc:

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

Regardez comme c'est facile maintenant! Pour plus d'informations, voir cette réponse, dont mes infos sont basées sur.


127
2018-04-10 09:57



Avec ES6 maintenant largement pris en charge, la meilleure réponse à cette question a changé. ES6 fournit le let et const mots-clés pour cette circonstance exacte. Au lieu de déconner avec des fermetures, nous pouvons simplement utiliser let pour définir une variable de portée de boucle comme ceci:

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

val pointera alors vers un objet spécifique à ce tour particulier de la boucle, et renverra la valeur correcte sans la notation de fermeture supplémentaire. Cela simplifie évidemment considérablement ce problème.

const est similaire à let avec la restriction supplémentaire que le nom de la variable ne peut pas être rebondi vers une nouvelle référence après l'affectation initiale.

Le support du navigateur est maintenant disponible pour ceux qui ciblent les dernières versions des navigateurs. const/let sont actuellement pris en charge dans les derniers Firefox, Safari, Edge et Chrome. Il est également pris en charge dans Node, et vous pouvez l'utiliser n'importe où en profitant des outils de construction comme Babel. Vous pouvez voir un exemple de travail ici: http://jsfiddle.net/ben336/rbU4t/2/

Docs ici:

Attention, cependant, que IE9-IE11 et Edge avant le support Edge 14 let mais obtenez le mauvais ci-dessus (ils ne créent pas une nouvelle i chaque fois, donc toutes les fonctions ci-dessus se connecteraient 3 comme ils le feraient si nous utilisions var). Edge 14 obtient finalement le droit.


121
2018-05-21 03:04



Une autre façon de le dire est que le i dans votre fonction est lié au moment de l'exécution de la fonction, pas le temps de créer la fonction.

Lorsque vous créez la fermeture, i est une référence à la variable définie dans la portée externe, pas une copie de celle-ci telle qu'elle était lorsque vous avez créé la fermeture. Il sera évalué au moment de l'exécution.

La plupart des autres réponses fournissent des moyens de contourner le problème en créant une autre variable qui ne changera pas la valeur pour vous.

Je pensais juste ajouter une explication pour plus de clarté. Pour une solution, personnellement, j'irais avec Harto parce que c'est la façon la plus explicite de le faire à partir des réponses ici. Tout le code affiché fonctionnera, mais j'opterais pour une usine de fermeture plutôt que d'écrire une pile de commentaires pour expliquer pourquoi je déclare une nouvelle variable (Freddy et 1800) ou si j'ai une syntaxe de fermeture bizarre (apphacker).


77
2018-04-15 06:48



Ce que vous devez comprendre, c'est que la portée des variables dans javascript est basée sur la fonction. C'est une différence importante que disons c # où vous avez une portée de bloc, et juste copier la variable à l'intérieur de la pour travailler.

L'emballer dans une fonction qui évalue renvoyer la fonction comme la réponse d'apphacker fera l'affaire, puisque la variable a maintenant la portée de la fonction.

Il y a aussi un mot-clé let au lieu de var, qui permettrait d'utiliser la règle d'étendue de bloc. Dans ce cas, la définition d'une variable à l'intérieur du pour ferait l'affaire. Cela dit, le mot-clé let n'est pas une solution pratique en raison de la compatibilité.

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



Voici une autre variante de la technique, similaire à celle de Bjorn (apphacker), qui vous permet d'assigner la valeur de la variable à l'intérieur de la fonction plutôt que de la passer en paramètre, ce qui peut être plus clair parfois:

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

Notez que quelle que soit la technique que vous utilisez, la index variable devient une sorte de variable statique, liée à la copie retournée de la fonction interne. C'est-à-dire que les modifications de sa valeur sont conservées entre les appels. Cela peut être très pratique.


49
2017-08-07 08:45