322 votes

SetTimeout dans la boucle for ne imprime pas des valeurs consécutives

J'ai ce script :

for (var i = 1; i <= 2; i++) {
    setTimeout(function() { alert(i) }, 100);
}

Mais 3 est alerté les deux fois, au lieu de 1 puis 2.

Y a-t-il un moyen de passer i, sans écrire la fonction comme une chaîne de caractères ?

9 votes

Aucune des réponses ici ne fonctionne. Chacune d'entre elles se contente de retarder pendant le temps défini, puis exécute immédiatement la boucle entière sans autres retards. En examinant le code de l'OP, il est clair qu'ils voulaient un retard à chaque itération.

1 votes

Il convient également de noter que si l'utilisateur SOUHAITAIT que les alertes se déclenchent en même temps, la mise en place de plusieurs setTimeout n'est PAS la meilleure façon de le faire.

14 votes

Utilisez le mot-clé "let" au lieu de var, cela résoudra le problème.

426voto

Pointy Points 172438

Vous devez organiser une copie distincte de "i" pour être présente pour chacune des fonctions timeout.

function doSetTimeout(i) {
  setTimeout(function() {
    alert(i);
  }, 100);
}

for (var i = 1; i <= 2; ++i)
  doSetTimeout(i);

Si vous ne faites pas quelque chose comme ceci (et il existe d'autres variations sur cette même idée), alors chacune des fonctions de gestionnaire de minuterie partagera la même variable "i". Quelle est la valeur de "i" une fois la boucle terminée? C'est 3! En utilisant une fonction intermédiaire, une copie de la valeur de la variable est faite. Puisque le gestionnaire de délai est créé dans le contexte de cette copie, il possède sa propre "i" privée à utiliser.

Éditer:

Il y a eu quelques commentaires au fil du temps où une certaine confusion était évidente sur le fait que la mise en place de quelques temporisations provoquait le déclenchement de tous les gestionnaires en même temps. Il est important de comprendre que le processus de mise en place du minuteur — les appels à setTimeout() — prend presque aucun temps du tout. Autrement dit, dire au système, "Veuillez appeler cette fonction après 1000 millisecondes" reviendra presque immédiatement, car le processus d'installation de la demande de délai dans la file d'attente du minuteur est très rapide.

Ainsi, si une suite de demandes de délai est effectuée, comme c'est le cas dans le code dans la question initiale et dans ma réponse, et que la valeur de retard de temps est la même pour chacune, alors une fois ce montant de temps écoulé, tous les gestionnaires de minuterie seront appelés les uns après les autres en succession rapide.

Si ce dont vous avez besoin est que les gestionnaires soient appelés à intervalles, vous pouvez soit utiliser setInterval(), qui est appelé exactement comme setTimeout() mais qui se déclenchera plus d'une fois après des retards répétés du montant demandé, ou bien vous pouvez établir les temporisations et multiplier la valeur d'attente par votre compteur d'itération. Autrement dit, pour modifier mon code exemple:

function doScaledTimeout(i) {
 setTimeout(function() {
   alert(I);
 }, i * 5000);
}

(Avec un délai de 100 millisecondes, l'effet ne sera pas très évident, alors j'ai augmenté le nombre à 5000.) La valeur de i est multipliée par la valeur de base du retard, donc appeler cela 5 fois dans une boucle donnera des retards de 5 secondes, 10 secondes, 15 secondes, 20 secondes et 25 secondes.

Mise à jour

Ici en 2018, il existe une solution alternative plus simple. Avec la nouvelle capacité de déclarer des variables dans des portées plus étroites que les fonctions, le code original fonctionnerait si modifié comme suit:

for (let i = 1; i <= 2; i++) {
  setTimeout(function() {
    alert(i)
  }, 100);
}

La déclaration let, contrairement à var, entraînera elle-même l'existence d'un i distinct pour chaque itération de la boucle.

7 votes

Ceci est la méthode préférée car elle ne provoque pas de définition de fonction à l'intérieur du corps de la boucle. Les autres fonctionneront, mais ne sont pas préférables (même si elles montrent l'incroyable côté bad-ass de JS ;) ).

1 votes

@JAAulde J'avoue que personnellement, je le ferais avec une fonction anonyme, mais de cette manière est plus jolie en tant qu'exemple.

1 votes

Je préfère personnellement les fonctions anonymes à cela car je ne veux pas configurer tout un tas de noms. Trop paresseux pour y penser.

198voto

Darin Dimitrov Points 528142

Vous pouvez utiliser une expression de fonction immédiatement invoquée (IIFE) pour créer une fermeture autour de setTimeout:

for (var i = 1; i <= 3; i++) {
    (function(index) {
        setTimeout(function() { alert(index); }, i * 1000);
    })(i);
}

2 votes

Bel exemple du monde réel des avantages de l'utilisation d'une fonction auto-invocante.

2 votes

Cela ne fonctionne pas : jsfiddle.net/Ljr9fq88

2 votes

IIFE n'est en rien qu'un raccourci pratique pour une fonction anonyme et une exécution immédiate - C'est en réalité ce que fait la réponse acceptée, en étapes complètes, sans raccourci - enveloppe l'appel de fonction dans une autre fonction, de sorte que la fonction interne obtienne une copie locale de l'argument de fonction externe.!!!

27voto

harto Points 28479

Le paramètre de fonction à setTimeout est en train de fermer sur la variable de boucle. La boucle se termine avant le premier délai et affiche la valeur actuelle de i, qui est 3.

Parce que les variables JavaScript ont uniquement portée de fonction, la solution est de passer la variable de boucle à une fonction qui définit le délai. Vous pouvez déclarer et appeler une telle fonction comme ceci:

for (var i = 1; i <= 2; i++) {
    (function (x) {
        setTimeout(function () { alert(x); }, 100);
    })(i);
}

0 votes

Cela ne fonctionne pas : jsfiddle.net/sq5n52xj

3 votes

Pour fonctionner, il suffit de multiplier le délai par i. comme ceci : setTimeout(function () { alert(x); }, i*100);

0 votes

Vous avez juste besoin de remplacer var par le mot-clé let et cela affichera le nombre 1 puis 2. Mais voici le piège une fois de plus, cela affichera à la fois 1 et 2 juste après 2 secondes. Si vous voulez afficher 1 et 2 à un intervalle de 1 seconde chacun, alors dans le rappel setTimeout, modifiez 1000 en i * 1000

23voto

Mevin Babu Points 883

Vous pouvez utiliser les arguments supplémentaires pour setTimeout pour passer des paramètres à la fonction de rappel.

for (var i = 1; i <= 2; i++) {
        setTimeout(function(j) { alert(j) }, 100, i);
}

Remarque : Cela ne fonctionne pas sur les navigateurs IE9 et précédents.

0 votes

Il existe un polyfill pour ce problème IE ici; developer.mozilla.org/fr/docs/Web/API/WindowTimers/…

0 votes

C'est gentil :) je n'ai jamais pensé que setTimeout accepte un troisième argument

9voto

Cody Points 1198

RÉPONSE?

Je l'utilise pour une animation d'ajout d'articles dans un panier - une icône de panier flotte jusqu'à la zone de panier depuis le bouton "ajouter" du produit, lorsque vous cliquez :

function addCartItem(opts) {
    for (var i=0; i

`

NOTEZ que la durée est en unités de temps n épochs.

Donc, en commençant au moment du clic, le départ de l'époque des animations (de CHAQUE animation) est le produit de chaque unité d'une seconde multiplié par le nombre d'articles.

époque : https://en.wikipedia.org/wiki/Epoch_(reference_date)

J'espère que cela vous aide !

`

1 votes

Vous pouvez également passer des arguments à la fonction de rappel comme suit :setTimeout(function(arg){...}, 1000*i, 'myArg');

0 votes

Finalement quelqu'un avec une réponse appropriée! Fonctionne à merveille

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X