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

2819

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 je voudrais qu'il affiche:

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 provoqué par l'utilisation d'écouteurs 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 code asynchrone, par exemple en utilisant 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));
}

Edit: Il est également apparent dans for inet for ofboucles:

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

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

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

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

Quelle est la solution à ce problème fondamental?

nickf
la source
55
Vous êtes sûr de ne pas vouloir funcsêtre un tableau, si vous utilisez des indices numériques? Juste un avertissement.
DanMan
23
C'est vraiment un problème déroutant. Cet article m'aide à le comprendre . Cela pourrait-il aussi aider les autres?
user3199690
4
Une autre solution simple et expliquée: 1) Les fonctions imbriquées ont accès à l'étendue "au-dessus" d'elles ; 2) une solution de fermeture ... "Une fermeture est une fonction ayant accès à la portée parent, même après la fermeture de la fonction parent".
Peter Krauss
2
Référez-vous à ce lien pour une meilleure compréhension de javascript.info/tutorial/advanced-functions
Saurabh Ahuja
35
Dans ES6 , une solution triviale consiste à déclarer la variable i avec let , qui est étendue au corps de la boucle.
Tomas Nikodym

Réponses:

2149

Eh bien, 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: fermetures

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

Puisqu'il n'y a pas de portée de bloc dans JavaScript - seulement la portée de la fonction - en encapsulant la création de la fonction dans une nouvelle fonction, vous vous assurez que la valeur de "i" reste comme vous le vouliez.


Solution ES5.1: forEach

Avec la disponibilité relativement répandue de la Array.prototype.forEachfonction (en 2015), il convient 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. Autrement dit, en supposant que vous disposez d'une sorte de tableau contenant des valeurs (références DOM, objets, etc.) 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 la .forEachboucle 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. S'il est utilisé dans un rappel asynchrone, il n'entrera en collision avec aucun des autres rappels établis aux autres étapes de l'itération.

S'il vous arrive de travailler dans jQuery, la $.each()fonction vous donne une capacité similaire.


Solution ES6: let

ECMAScript 6 (ES6) lance une nouvelle letet des constmots - clés qui sont différentes que la portée se les varvariables à base. Par exemple, dans une boucle avec un letindex basé sur, chaque itération à travers la boucle aura une nouvelle variable iavec une portée de boucle, donc votre code fonctionnera comme vous vous y attendez. Il existe de nombreuses ressources, mais je recommanderais le post de portée de bloc de 2ality comme une excellente source d'informations.

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

Attention, cependant, IE9-IE11 et Edge avant la prise en charge d'Edge 14 letmais se trompent (ils n'en créent pas à ichaque fois, donc toutes les fonctions ci-dessus enregistreraient 3 comme si nous les utilisions var). Edge 14 réussit enfin.

harto
la source
7
n'est pas function createfunc(i) { return function() { console.log("My value: " + i); }; }encore fermé car il utilise la variable i?
ア レ ッ ク ス
55
Malheureusement, cette réponse est obsolète et personne ne verra la bonne réponse en bas - l'utilisation Function.bind()est définitivement préférable maintenant, voir stackoverflow.com/a/19323214/785541 .
Wladimir Palant
81
@Wladimir: Votre suggestion qui .bind()est "la bonne réponse" n'est pas juste. Ils ont chacun leur place. Avec .bind()vous ne pouvez pas lier d'arguments sans lier la thisvaleur. Vous obtenez également une copie de l' iargument sans la possibilité de le muter entre les appels, ce qui est parfois nécessaire. Ce sont donc des constructions très différentes, sans compter que les .bind()implémentations ont été historiquement lentes. Bien sûr, dans l'exemple simple, cela fonctionnerait, mais les fermetures sont un concept important à comprendre, et c'est de cela qu'il s'agissait.
monstre cookie
8
Veuillez cesser d'utiliser ces hacks de fonction de retour, utilisez plutôt [] .forEach ou [] .map car ils évitent de réutiliser les mêmes variables de portée.
Christian Landgren
32
@ChristianLandgren: Cela n'est utile que si vous itérez un tableau. Ces techniques ne sont pas des "hacks". Ce sont des connaissances essentielles.
379

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 que la réponse.bind la plus récente de @ Aust sur l'utilisation est la meilleure façon de faire ce genre de chose maintenant. Il y a aussi lo tableau de bord / underscore est _.partialquand vous n'avez pas besoin ou si vous voulez jouer avec bind« s thisArg.

Bjorn
la source
2
une explication sur le }(i));?
aswzen
3
@aswzen Je pense que cela passe icomme argument indexà la fonction.
Jet Blue
il crée en fait un index de variable locale.
Abhishek Singh
1
Invoquer immédiatement l'expression de fonction, également appelée IIFE. (i) est l'argument de l'expression de fonction anonyme qui est invoquée immédiatement et l'index devient défini à partir de i.
Oeufs
348

Un autre moyen qui n'a pas encore été mentionné 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]();
}

MISE À JOUR

Comme indiqué par @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]();
}

Aust
la source
C'est aussi ce que je fais ces jours-ci, j'aime aussi les tirets bas / tirets bas_.partial
Bjorn
17
.bind()sera largement obsolète avec les fonctionnalités d'ECMAScript 6. En outre, cela crée en fait deux fonctions par itération. D'abord l'anonyme, puis celui généré par .bind(). Une meilleure utilisation serait de le créer à l'extérieur de la boucle, puis .bind()à l'intérieur.
5
@squint @mekdev - Vous avez tous les deux raison. Mon premier exemple a été écrit rapidement pour montrer comment il bindest utilisé. J'ai ajouté un autre exemple selon vos suggestions.
Aust
5
Je pense qu'au lieu de gaspiller le calcul sur deux boucles O (n), faites simplement pour (var i = 0; i <3; i ++) {log.call (this, i); }
user2290820
1
.bind () fait ce que la réponse acceptée suggère PLUS this.
niry
269

À l'aide d'une expression de fonction immédiatement invoquée , la manière la plus simple et la plus lisible pour entourer 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 idans la fonction anonyme que nous définissons comme index. Cela crée une fermeture, où la variable iest enregistrée pour une utilisation ultérieure dans toute fonctionnalité asynchrone au sein de l'IIFE.

neurosnap
la source
10
Pour plus de lisibilité du code et pour éviter toute confusion quant à ce qui iest quoi, je renommerais le paramètre de fonction en index.
Kyle Falconer
5
Comment cette technique utiliseriez - vous pour définir les tableaux funcs décrits dans la question initiale?
Nico
@Nico De la même manière que dans la question d'origine, sauf que vous utiliseriez à la indexplace de i.
JLRishe
@JLRishevar 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
@Nico Dans le cas particulier d'OP, ils sont juste en train d'itérer sur les nombres, donc ce ne serait pas un bon cas pour .forEach(), mais la plupart du temps, quand on commence avec un tableau, forEach()c'est un bon choix, comme:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
JLRishe
164

Un peu tard pour la fête, mais j'explorais ce problème aujourd'hui et j'ai remarqué que la plupart des réponses ne traitent pas complètement de la façon dont Javascript traite les portées, ce qui est essentiellement ce que cela se résume.

Ainsi, comme beaucoup d'autres l'ont mentionné, le problème est que la fonction interne fait référence à la même ivariable. Alors pourquoi ne créons-nous pas simplement une nouvelle variable locale à chaque itération, et avons-nous la référence de 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]();
}

Tout comme auparavant, où chaque fonction interne a sorti 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 la sienne ilocal?

Il s'avère que c'est le problème. Chaque itération partage la même étendue, donc chaque itération après la première n'est que l'écrasement 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 le voir en vérifiant ilocalavant de le déclarer à 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 délicat. Même si vous redéclarez une variable, Javascript ne lancera pas d'erreur et JSLint ne lancera même pas d'avertissement. C'est aussi pourquoi la meilleure façon de résoudre ce problème 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 car les portées internes "renferment" les portées externes.

Fermetures

Cela signifie également que les fonctions internes "conservent" 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 retournons 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]();
}

La création de 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é, garantissant que les ilocalvariables ne se heurtent pas et ne se remplacent pas. Quelques optimisations mineures donnent la réponse finale que de nombreux autres utilisateurs de SO ont donnée:

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

Mise à jour

Avec ES6 désormais intégré, nous pouvons désormais utiliser le nouveau letmot clé pour créer des variables de portée 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, consultez cette réponse , sur laquelle mes informations sont basées.

woojoo666
la source
J'aime aussi la façon dont vous avez expliqué la méthode IIFE. Je cherchais ça. Je vous remercie.
CapturedTree du
4
Il existe maintenant une chose telle que l'étendue des blocs en JavaScript à l'aide des mots let- constclés et . Si cette réponse devait s'élargir pour l'inclure, elle serait beaucoup plus utile à l'échelle mondiale à mon avis.
@TinyGiant c'est sûr, j'ai ajouté quelques informations sur letet lié une explication plus complète
woojoo666
@ woojoo666 pourrait aussi votre réponse travail pour appeler deux URL dans une boucle alternative comme ceci: i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (pourrait remplacer window.open () par getelementbyid ......)
nutty about natty
@nuttyaboutnatty désolé pour une réponse si tardive. Il ne semble pas que le code de votre exemple fonctionne déjà. Vous n'utilisez pas ivos fonctions de temporisation, vous n'avez donc pas besoin de fermeture
woojoo666
151

L'ES6 étant désormais largement pris en charge, la meilleure réponse à cette question a changé. ES6 fournit les mots clés letet constpour cette circonstance exacte. Au lieu de jouer avec les fermetures, nous pouvons simplement utiliser letpour définir une variable d'étendue de boucle comme ceci:

var funcs = [];

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

valpointera ensuite sur 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 la restriction supplémentaire selon laquelle le nom de variable ne peut pas être renvoyé à une nouvelle référence après l'affectation initiale.

La prise en charge du navigateur est désormais disponible pour ceux qui ciblent les dernières versions des navigateurs. const/ letsont 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 tirant parti d'outils de construction comme Babel. Vous pouvez voir un exemple de travail ici: http://jsfiddle.net/ben336/rbU4t/2/

Documents ici:

Attention, cependant, IE9-IE11 et Edge avant la prise en charge d'Edge 14 letmais se trompent (ils n'en créent pas à ichaque fois, donc toutes les fonctions ci-dessus enregistreraient 3 comme si nous les utilisions var). Edge 14 réussit enfin.

Ben McCormick
la source
Malheureusement, «let» n'est toujours pas entièrement pris en charge, en particulier dans les mobiles. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
MattC
2
Depuis juin '16, let est pris en charge dans toutes les principales versions de navigateur, à l'exception d'iOS Safari, Opera Mini et Safari 9. Les navigateurs Evergreen le prennent en charge. Babel le transpile correctement pour conserver le comportement attendu sans que le mode haute conformité soit activé.
Dan Pantry
@DanPantry ouais sur le temps pour une mise à jour :) Mis à jour pour mieux refléter l'état actuel des choses, y compris en ajoutant une mention de const, des liens doc et de meilleures informations de compatibilité.
Ben McCormick
N'est-ce pas pourquoi nous utilisons babel pour transpiler notre code afin que les navigateurs qui ne prennent pas en charge ES6 / 7 puissent comprendre ce qui se passe?
pixel 67
87

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 au moment de la création de la fonction.

Lorsque vous créez la fermeture, i est une référence à la variable définie dans la portée extérieure, et non 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 car c'est la façon la plus explicite de le faire à partir des réponses ici. N'importe quel code publié fonctionnera, mais j'opterais pour une usine de fermeture plutôt que d'avoir à écrire une pile de commentaires pour expliquer pourquoi je déclare une nouvelle variable (Freddy et 1800) ou si j'ai une étrange syntaxe de fermeture intégrée (apphacker).

Darren Clark
la source
71

Ce que vous devez comprendre, c'est que la portée des variables en 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 simplement copier la variable dans une à l'intérieur du for fonctionnera.

L'encapsuler dans une fonction qui évalue le retour de la fonction comme la réponse d'Apphacker fera l'affaire, car la variable a maintenant la portée de la fonction.

Il existe également un mot clé let au lieu de var, qui permettrait d'utiliser la règle d'étendue de bloc. Dans ce cas, définir une variable à l'intérieur du for 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]();
}

eglasius
la source
@nickf quel navigateur? comme je l'ai dit, il y a des problèmes de compatibilité, avec cela je veux dire de sérieux problèmes de compatibilité, comme je ne pense pas que let soit pris en charge dans IE.
eglasius
1
@nickf oui, vérifiez cette référence: developer.mozilla.org/En/New_in_JavaScript_1.7 ... vérifiez la section des définitions de let, il y a un exemple onclick à l'intérieur d'une boucle
eglasius
2
@nickf hmm, en fait, vous devez spécifier explicitement la version: <script type = "application / javascript; version = 1.7" /> ... Je ne l'ai utilisé nulle part à cause de la restriction IE, ce n'est tout simplement pas pratique :(
eglasius
vous pouvez voir le support du navigateur pour les différentes versions ici es.wikipedia.org/wiki/Javascript
eglasius
59

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

var funcs = [];
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 indexvariable 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.

Boann
la source
Merci et votre solution fonctionne. Mais je voudrais demander pourquoi cela fonctionne, mais permuter la varligne et la returnligne ne fonctionnerait pas? Merci!
midnite
@midnite Si vous avez échangé varet que returnla variable ne serait pas affectée avant d'avoir renvoyé la fonction interne.
Boann
53

Ceci décrit l'erreur courante lors de l'utilisation de fermetures en JavaScript.

Une fonction définit un nouvel environnement

Considérer:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

Pour chaque makeCounterinvocation, {counter: 0}un nouvel objet est créé. De plus, une nouvelle copie de obj est également créée pour référencer le nouvel objet. Ainsi, counter1etcounter2 sont indépendants les uns des autres.

Fermetures en boucles

L'utilisation d'une fermeture dans une boucle est délicate.

Considérer:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

Notez que counters[0]et necounters[1] sont pas indépendants. En fait, ils fonctionnent sur le même obj!

En effet, il n'y a qu'une seule copie de objpartagée sur toutes les itérations de la boucle, peut-être pour des raisons de performances. Même si {counter: 0}un nouvel objet est créé à chaque itération, la même copie de objsera simplement mise à jour avec une référence à l'objet le plus récent.

La solution consiste à utiliser une autre fonction d'assistance:

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

Cela fonctionne parce que les variables locales dans l'étendue de la fonction directement, ainsi que les variables d'argument de fonction, se voient allouer de nouvelles copies à l'entrée.


la source
Petite précision: dans le premier exemple de fermetures en boucles, les compteurs [0] et les compteurs [1] ne sont pas indépendants non pas pour des raisons de performances. La raison en est qu'elle var obj = {counter: 0};est évaluée avant l'exécution de tout code, comme indiqué dans: MDN var : les déclarations var, où qu'elles se produisent, sont traitées avant l'exécution de tout code.
Charidimos
50

La solution la plus simple serait,

À la place d'utiliser:

var funcs = [];
for(var i =0; i<3; i++){
    funcs[i] = function(){
        alert(i);
    }
}

for(var j =0; j<3; j++){
    funcs[j]();
}

qui alerte "2", 3 fois. En effet, les fonctions anonymes créées dans la boucle for partagent la même fermeture et, dans cette fermeture, la valeur de iest la même. Utilisez ceci pour empêcher la fermeture partagée:

var funcs = [];
for(var new_i =0; new_i<3; new_i++){
    (function(i){
        funcs[i] = function(){
            alert(i);
        }
    })(new_i);
}

for(var j =0; j<3; j++){
    funcs[j]();
}

L'idée derrière cela est d'encapsuler le corps entier de la boucle for avec une IIFE (Immediateely Invoked Function Expression) et de passer new_ien paramètre et de le capturer en tant que i. Étant donné que la fonction anonyme est exécutée immédiatement, lei valeur est différente pour chaque fonction définie à l'intérieur de la fonction anonyme.

Cette solution semble répondre à un tel problème car elle nécessitera des modifications minimes du code d'origine souffrant de ce problème. En fait, c'est par conception, cela ne devrait pas être un problème du tout!

Kemal Dağ
la source
2
Lisez une fois quelque chose de similaire dans un livre. Je préfère cela aussi, car vous n'avez pas à toucher à votre code existant (autant) et il devient évident pourquoi vous l'avez fait, une fois que vous avez appris le modèle de fonction auto-appelant: pour piéger cette variable dans la nouvelle création portée.
DanMan
1
@DanMan Merci. Les fonctions anonymes auto-appelantes sont un très bon moyen de remédier au manque de portée de variable de niveau bloc de javascript.
Kemal Dağ
3
Auto-appel ou auto-invocation n'est pas le terme approprié pour cette technique, IIFE (Immediately-Invoked Function Expression) est plus précis. Ref: benalman.com/news/2010/11/…
jherax
31

essayez celui-ci plus court

  • pas de tableau

  • pas de supplément pour la boucle


for (var i = 0; i < 3; i++) {
    createfunc(i)();
}

function createfunc(i) {
    return function(){console.log("My value: " + i);};
}

http://jsfiddle.net/7P6EN/

yilmazburk
la source
1
Votre solution semble sortir correctement mais elle utilise inutilement des fonctions, pourquoi pas simplement console.log la sortie? La question initiale concerne la création de fonctions anonymes ayant la même fermeture. Le problème était, comme ils ont une seule fermeture, la valeur de i est la même pour chacun d'eux. J'espère que vous l'avez compris.
Kemal Dağ
30

Voici une solution simple qui utilise forEach(fonctionne sur IE9):

var funcs = [];
[0,1,2].forEach(function(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
}

Tirages:

My value: 0
My value: 1
My value: 2
Daryl
la source
27

Le principal problème avec le code affiché par l'OP est qu'il in'est jamais lu jusqu'à la deuxième boucle. Pour démontrer, imaginez voir une erreur à l'intérieur du code

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

L'erreur ne se produit pas tant qu'elle funcs[someIndex]n'est pas exécutée (). En utilisant cette même logique, il devrait être évident que la valeur de in'est pas non plus collectée avant ce point non plus. Une fois que les finitions de boucle d' origine, i++apporte ià la valeur 3qui se traduit par la condition i < 3défaillante et la boucle sans fin. À ce stade, iest 3et donc quand funcs[someIndex]()est utilisé, eti évalué, il est 3 - à chaque fois.

Pour dépasser cela, vous devez évaluer au ifur et à mesure qu'il se produit. Notez que cela s'est déjà produit sous forme defuncs[i] (où il y a 3 index uniques). Il existe plusieurs façons de capturer cette valeur. La première consiste à le passer en paramètre à une fonction qui est déjà présentée de plusieurs façons ici.

Une autre option consiste à construire un objet fonction qui pourra se refermer sur la variable. Cela peut être accompli ainsi

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};
Travis J
la source
23

Les fonctions JavaScript "ferment" la portée à laquelle elles ont accès lors de la déclaration et conservent l'accès à cette portée même lorsque les variables de cette portée changent.

var funcs = []

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

Chaque fonction du tableau ci-dessus se ferme sur la portée globale (globale, simplement parce que c'est la portée dans laquelle ils sont déclarés).

Plus tard, ces fonctions sont appelées à enregistrer la valeur la plus récente de idans la portée globale. C'est la magie et la frustration de la fermeture.

"Les fonctions JavaScript se ferment sur la portée dans laquelle elles sont déclarées et conservent l'accès à cette portée même lorsque les valeurs des variables à l'intérieur de cette portée changent."

Utiliser letau lieu de varrésout cela en créant une nouvelle portée à chaque forexécution de la boucle, en créant une portée séparée pour chaque fonction à fermer. Diverses autres techniques font la même chose avec des fonctions supplémentaires.

var funcs = []

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

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

( letrend le bloc de variables limité. Les blocs sont indiqués par des accolades, mais dans le cas de la boucle for, la variable d'initialisation, idans notre cas, est considérée comme déclarée dans les accolades.)

Costa
la source
1
J'ai eu du mal à comprendre ce concept jusqu'à ce que je lise cette réponse. Il touche à un point très important - la valeur de iest fixée à la portée mondiale. Lorsque la forboucle se termine, la valeur globale de iest maintenant 3. Par conséquent, chaque fois que cette fonction est invoquée dans le tableau (en utilisant, disons funcs[j]), idans cette fonction fait référence à la ivariable globale (qui est 3).
Modermo
13

Après avoir lu diverses solutions, j'aimerais ajouter que la raison pour laquelle ces solutions fonctionnent est de s'appuyer sur le concept de chaîne de portée . C'est la façon dont JavaScript résout une variable pendant l'exécution.

  • Chaque définition de fonction forme une étendue composée de toutes les variables locales déclarées par varet son arguments.
  • Si nous avons une fonction interne définie à l'intérieur d'une autre fonction (externe), cela forme une chaîne et sera utilisé pendant l'exécution
  • Lorsqu'une fonction est exécutée, le runtime évalue les variables en recherchant la chaîne de portée . Si une variable peut être trouvée à un certain point de la chaîne, elle arrêtera la recherche et l'utilisera, sinon elle continue jusqu'à ce que la portée globale atteinte à laquelle appartient window.

Dans le code initial:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

Une fois funcsexécuté, la chaîne de portée sera function inner -> global. Étant donné que la variable ine peut pas être trouvée dans function inner(ni déclarée à l'aide varni passée comme arguments), elle continue de rechercher, jusqu'à ce que la valeur de isoit finalement trouvée dans la portée globale qui est window.i.

En l'enveloppant dans une fonction externe, définissez explicitement une fonction d'aide comme l'a fait harto ou utilisez une fonction anonyme comme l'a fait Bjorn :

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

Une fois funcsexécuté, la chaîne de portée le sera désormais function inner -> function outer. Cette heure ipeut être trouvée dans la portée de la fonction externe qui est exécutée 3 fois dans la boucle for, chaque fois que la valeur est iliée correctement. Il n'utilisera pas la valeur de window.il'exécution interne.

Plus de détails peuvent être trouvés ici.
Cela inclut l'erreur courante de créer une fermeture dans la boucle comme ce que nous avons ici, ainsi que la raison pour laquelle nous avons besoin d'une fermeture et de la performance.

wpding
la source
Nous écrivons rarement cet exemple de code en vrai, mais je pense qu'il sert de bon exemple pour comprendre le fondamental. Une fois que nous avons la portée à l'esprit et comment ils se sont enchaînés, il est plus clair de voir pourquoi d'autres méthodes `` modernes '' comme Array.prototype.forEach(function callback(el) {})naturellement fonctionnent: le rappel qui est passé en forme naturellement la portée de l'habillage avec el correctement lié à chaque itération de forEach. Ainsi, chaque fonction interne définie dans le rappel pourra utiliser la bonne elvaleur
wpding
13

Avec les nouvelles fonctionnalités de l'ES6, la portée au niveau du bloc est gérée:

var funcs = [];
for (let 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 (let j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Le code de la question OP est remplacé par letau lieu de var.

Prithvi Uppalapati
la source
constfournit le même résultat et doit être utilisé lorsque la valeur d'une variable ne change pas. Cependant, l'utilisation de l' constintérieur de l'initialiseur de la boucle for n'est pas implémentée correctement dans Firefox et n'a pas encore été corrigée. Au lieu d'être déclaré à l'intérieur du bloc, il est déclaré à l'extérieur du bloc, ce qui entraîne une redéclaration de la variable, ce qui entraîne à son tour une erreur. L'utilisation de l' letintérieur de l'initialiseur est correctement implémentée dans Firefox, donc ne vous inquiétez pas.
10

Je suis surpris que personne n'ait encore suggéré d'utiliser la forEachfonction pour mieux éviter de (ré) utiliser des variables locales. En fait, je n'utilise for(var i ...)plus du tout pour cette raison.

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

// modifié pour utiliser à la forEachplace de la carte.

Christian Landgren
la source
3
.forEach()est une bien meilleure option si vous ne cartographiez rien, et Daryl l'a suggéré 7 mois avant de publier, donc il n'y a rien à être surpris.
JLRishe
Cette question ne concerne pas la boucle sur un tableau
jherax
Eh bien, il veut créer un tableau de fonctions, cet exemple montre comment faire cela sans impliquer une variable globale.
Christian Landgren
9

Cette question montre vraiment l'histoire de JavaScript! Maintenant, nous pouvons éviter la portée des blocs avec les fonctions fléchées et gérer les boucles directement à partir des nœuds DOM à l'aide des méthodes Object.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>

sidhuko
la source
8

La raison pour laquelle votre exemple d'origine 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 effet, avoir 3 méthodes sur un objet avec une seule ivariable. Ils ont tous imprimé la même valeur.

jottos
la source
8

Tout d'abord, comprenez ce qui ne va pas avec ce code:

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
}

Ici, lorsque le funcs[]tableau est en cours d'initialisation, iest incrémenté, le funcstableau est initialisé et la taille du functableau devient 3, donc i = 3,. Maintenant, lorsque le funcs[j]()est appelé, il utilise à nouveau la variable i, qui a déjà été incrémentée à 3.

Maintenant, pour résoudre ce problème, nous avons de nombreuses options. En voici deux:

  1. Nous pouvons initialiser iavec letou initialiser une nouvelle variable indexavec letet la rendre égale à i. Ainsi, lorsque l'appel est en cours, indexsera utilisé et sa portée se terminera après l'initialisation. Et pour appeler, indexsera à nouveau initialisé:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
  2. Une autre option peut être d'introduire un tempFuncqui renvoie la fonction réelle:

    var funcs = [];
    function tempFunc(i){
        return function(){
            console.log("My value: " + i);
        };
    }
    for (var i = 0; i < 3; i++) {  
        funcs[i] = tempFunc(i);                                     
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
Ali Kahoot
la source
8

Utilisez une structure de fermeture , cela réduirait votre supplément de boucle. Vous pouvez le faire en une seule boucle for:

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

Nous vérifierons ce qui se passe réellement lorsque vous déclarez varet let un par un.

Cas 1 : utilisationvar

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

Ouvrez maintenant la fenêtre de votre console Chrome en appuyant sur F12 et actualisez la page. Développer toutes les 3 fonctions à l'intérieur du tableau.Vous verrez une propriété appelée [[Scopes]].Agrandir celle-ci. Vous verrez un objet tableau appelé "Global", développez-le. Vous trouverez une propriété 'i'déclarée dans l'objet qui a la valeur 3.

entrez la description de l'image ici

entrez la description de l'image ici

Conclusion:

  1. Lorsque vous déclarez une variable en utilisant en 'var'dehors d'une fonction, elle devient une variable globale (vous pouvez vérifier en tapant iou window.idans la fenêtre de la console. Elle renverra 3).
  2. La fonction annominale que vous avez déclarée n'appellera pas et ne vérifiera pas la valeur à l'intérieur de la fonction, sauf si vous appelez les fonctions.
  3. Lorsque vous appelez la fonction, console.log("My value: " + i)prend la valeur de son Globalobjet et affiche le résultat.

CAS2: utilisation de let

Remplacez maintenant le 'var'par'let'

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

Faites la même chose, allez dans les étendues. Vous verrez maintenant deux objets "Block"et "Global". Maintenant, développez l' Blockobjet, vous verrez que «i» y est défini, et l'étrange est que, pour chaque fonction, la valeur if iest différente (0, 1, 2).

entrez la description de l'image ici

Conclusion:

Lorsque vous déclarez une variable en utilisant 'let'même en dehors de la fonction mais à l'intérieur de la boucle, cette variable ne sera pas une variable globale, elle deviendra une Blockvariable de niveau qui n'est disponible que pour la même fonction. C'est la raison pour laquelle nous obtenons une valeur idifférente pour chaque fonction lorsque nous invoquons les fonctions.

Pour plus de détails sur le fonctionnement plus proche, veuillez consulter le didacticiel vidéo génial https://youtu.be/71AtaJpJHw0

Bimal Das
la source
4

Vous pouvez utiliser un module déclaratif pour les listes de données telles que query-js (*). Dans ces situations, je trouve personnellement une approche déclarative moins surprenante

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

Vous pouvez ensuite utiliser votre deuxième boucle et obtenir le résultat attendu ou vous pouvez le faire

funcs.iterate(function(f){ f(); });

(*) Je suis l'auteur de query-js et donc partisan de son utilisation, alors ne prenez pas mes mots comme recommandation pour ladite bibliothèque uniquement pour l'approche déclarative :)

Rune FS
la source
1
J'aimerais une explication du vote négatif. Le code résout le problème actuel. Il serait utile de savoir comment potentiellement améliorer le code
Rune FS
1
Qu'est-ce que c'est Query.range(0,3)? Cela ne fait pas partie des balises de cette question. De plus, si vous utilisez une bibliothèque tierce, vous pouvez fournir le lien de la documentation.
jherax
1
@jherax ce sont ou bien sûr des améliorations évidentes. Merci pour le commentaire. J'aurais pu jurer qu'il y avait déjà un lien. Sans cela, le post était assez inutile, je suppose :). Mon idée initiale de le garder était parce que je n'essayais pas de pousser l'utilisation de ma propre bibliothèque mais plutôt l'idée déclarative. Cependant, en toute lucidité, je suis entièrement d'accord pour dire que le lien devrait être là
Rune FS
4

Je préfère utiliser la forEachfonction, qui a sa propre fermeture avec la création d'une pseudo-plage:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

for (var j = 0; j < 3; j++) {
    funcs[j](); // 0, 1, 2
}

Cela semble plus laid que les gammes dans d'autres langues, mais à mon humble avis moins monstrueux que d'autres solutions.

Rax Wunter
la source
Je préfère quoi? Cela semble être un commentaire en réponse à une autre réponse. Il ne répond pas du tout à la question réelle (puisque vous n'affectez pas de fonction, à appeler ultérieurement, n'importe où).
Quentin
C'est exactement lié au problème mentionné: comment itérer en toute sécurité sans problèmes de fermeture
Rax Wunter
Maintenant, cela ne semble pas très différent de la réponse acceptée.
Quentin
Non. Dans la réponse acceptée, il est suggéré d'utiliser "un tableau", mais nous traitons une plage dans la réponse, ce sont des choses absolument différentes, qui malheureusement n'ont pas une bonne solution en js, donc ma réponse essaie de résoudre le problème d'une manière bonne et pratique
Rax Wunter
@Quentin, je recommanderais d'enquêter sur la solution avant de minus
Rax Wunter
4

Et encore une autre solution: au lieu de créer une autre boucle, liez simplement le thisà la fonction de retour.

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

En liant cela , résout également le problème.

pixel 67
la source
3

De nombreuses solutions semblent correctes, mais elles ne mentionnent pas son nom, Curryingqui est un modèle de conception de programmation fonctionnelle pour des situations comme ici. 3 à 10 fois plus rapide que bind en fonction du navigateur.

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

function curryShowValue(i) {
  return function showValue() {
    console.log("My value: " + i);
  }
}

Voir le gain de performances dans différents navigateurs .

Pawel
la source
@TinyGiant L'exemple avec la fonction renvoyée est toujours optimisé pour les performances. Je ne voudrais pas sauter sur les fonctions fléchées comme tous les blogueurs JavaScript. Ils ont l'air cool et propres mais favorisent l'écriture de fonctions en ligne au lieu d'utiliser des fonctions prédéfinies. Cela peut être un piège non évident dans les endroits chauds. Un autre problème est qu'ils ne sont pas seulement du sucre syntaxique car ils exécutent des liaisons inutiles créant ainsi des fermetures de wrapping.
Pawel
2
Avertissement aux futurs lecteurs: cette réponse applique à tort le terme Currying . "Le currying consiste à décomposer une fonction qui prend plusieurs arguments en une série de fonctions qui font partie des arguments." . Ce code ne fait rien de tel. Tout ce que vous avez fait ici est de prendre le code de la réponse acceptée, de déplacer certaines choses, de changer le style et de nommer un peu, puis de l'appeler currying, ce qui n'est pas catégoriquement.
3

Votre code ne fonctionne pas, car il fait:

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

Maintenant, la question est, quelle est la valeur de variable ilorsque la fonction est appelée? Étant donné que la première boucle est créée avec la condition de i < 3, elle s'arrête immédiatement lorsque la condition est fausse, ce qui est le cas i = 3.

Vous devez comprendre que, au moment où vos fonctions sont créées, aucun de leur code n'est exécuté, il n'est enregistré que pour plus tard. Et donc quand ils sont appelés plus tard, l'interprète les exécute et demande: "Quelle est la valeur actuelle de i?"

Ainsi, votre objectif est d'enregistrer d'abord la valeur de ipour fonctionner et uniquement après cela, d'enregistrer la fonction dans funcs. Cela pourrait être fait par exemple de cette façon:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function(x) {            // and store them in funcs
        console.log("My value: " + x); // each should log its value.
    }.bind(null, i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

De cette façon, chaque fonction aura sa propre variable xet nous la mettons xà la valeur de idans chaque itération.

Ce n'est qu'une des multiples façons de résoudre ce problème.

Buksy
la source
3
var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function(param) {          // and store them in funcs
    console.log("My value: " + param); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j](j);                      // and now let's run each one to see with j
}
Ashish Yadav
la source
3

Utilisez let (portée bloquée) au lieu de var.

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

BG Hari Prasad
la source