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

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

Il produit ceci :

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

Alors que j'aimerais qu'il sorte :

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


Le même problème se produit lorsque le retard dans l'exécution de la fonction est causé par l'utilisation de récepteurs d'événements :

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>

... ou du code asynchrone, par exemple en utilisant des 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));
}

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

Solution

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

Solution classique : Les fermetures

Ce que vous voulez faire, c'est lier la variable dans chaque fonction à une valeur distincte, 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++) {
  // and now let's run each one to see
  funcs[j]();
}

Comme il n'y a pas de portée de bloc en JavaScript - seulement une portée de fonction - en enveloppant la création de fonction dans une nouvelle fonction, vous vous assurez que la valeur de "i" reste telle que vous l'aviez prévue.


Solution 2015 : forEach

Avec la disponibilité relativement répandue de la fonction Array.prototype.forEach (en 2015), il est intéressant de noter que dans les situations impliquant une itération principalement sur un tableau 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 avez une sorte de tableau contenant des valeurs (références DOM, objets, peu importe), et que le problème se pose de mettre en place des callbacks spécifiques à chaque élément, vous pouvez faire ceci :

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 la boucle .forEach sera sa propre fermeture. Le paramètre passé à ce gestionnaire est l'élément du tableau spécifique à cette étape particulière de l'itération. S'il est utilisé dans un callback asynchrone, il n'entrera en collision avec aucun des autres callbacks établis à d'autres étapes de l'itération.

Si vous travaillez avec jQuery, la fonction $.each() vous offre une possibilité similaire.


Solution ES6 : let

ECMAScript 6 (ES6) introduit de nouveaux mots-clés let et const qui sont scopés différemment des variables basées sur var. Par exemple, dans une boucle avec un index basé sur let, chaque itération dans la boucle aura une nouvelle valeur de i où chaque valeur est scopée à l'intérieur de la boucle, donc votre code fonctionnera comme vous l'attendez. Il existe de nombreuses ressources, mais je recommande 2ality's block-scoping post comme une excellente source d'information.

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

Attention, cependant, IE9-IE11 et Edge avant Edge 14 supportent let mais ne font pas ce qui précède (ils ne créent pas un nouveau i à chaque fois, donc toutes les fonctions ci-dessus enregistreraient 3 comme elles le feraient si nous utilisions var). Edge 14 a finalement réussi à le faire.

Commentaires (23)

Essayez :

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

Edit (2014) :

Personnellement, je pense que la réponse plus récente sur l'utilisation de .bind de @Aust est la meilleure façon de faire ce genre de chose maintenant. Il y a aussi le _.partial de lo-dash/underscore quand vous n'avez pas besoin ou envie de vous embêter avec le thisArg de bind.

Commentaires (3)

La raison pour laquelle votre exemple original n'a pas fonctionné est que toutes les fermetures que vous avez créées dans la boucle faisaient référence au même cadre. En fait, vous avez 3 méthodes sur un objet avec une seule variable i. Elles ont toutes imprimé la même valeur.

Commentaires (0)