Examinons ce snippet JavaScript :
for (var compteur = 0; compteur < 5; compteur++) {
setTimeout(function() {
console.log(compteur);
}, 0);
}
Le résultat produit cinq fois la valeur 5. Cela s'explique par l'exécution asynchrone de setTimeout et la portée fonctionnelle de var.
Les temporisations sont placées dans la file d'attente des tâches et ne s'exécutent qu'après la fin de la boucle synchrone. À ce moment, compteur vaut déjà 5, car toutes les itérations ont eu lieu. Chaque rappel partage la même variable, d'où l'affichage répété.
Voici une illustration simplifiée de l'ordre d'exécution :
// Exécution synchrone d'abord
for (var compteur = 0; compteur < 5; compteur++) {
// Enregistrement des tâches asynchrones
}
// Puis, exécution des rappels
setTimeout(() => console.log(compteur)); // compteur est 5
// ... répété cinq fois
Pour afficher 0, 1, 2, 3, 4, on peut capturer la valeur courante de l'itération. Plusieurs approches sont possibles :
1. Utilisation de let
let crée une portée de bloc, isolant chaque itération :
for (let index = 0; index < 5; index++) {
setTimeout(() => console.log(index), 0);
}
// Affiche 0, 1, 2, 3, 4
Chaque rappel référence une variable index distincte grâce à la portée de bloc.
2. Fonction immédiatmeent invoquée (IIFE)
On encapsule la valeur dans une fonction pour créer une fremeture :
for (var compteur = 0; compteur < 5; compteur++) {
(function(valeur) {
setTimeout(() => console.log(valeur), 0);
})(compteur);
}
// Affiche 0, 1, 2, 3, 4
La fonction valeur capture la valeur actuelle de compteur à chaque passage.
3. Passage de paramètre à setTimeout
On utilise le troisième argument de setTimeout pour transmettre la donnée :
for (var compteur = 0; compteur < 5; compteur++) {
setTimeout((num) => {
console.log(num);
}, 0, compteur);
}
// Affiche 0, 1, 2, 3, 4
Ici, compteur est passé directement au rappel comme argument.
4. Avec Promise
On peut utiliser les promesses pour retarder l'exécution :
for (var compteur = 0; compteur < 5; compteur++) {
Promise.resolve(compteur).then(val => {
setTimeout(() => console.log(val), 0);
});
}
// Affiche 0, 1, 2, 3, 4
La promesse résout immédiatement la valeur, la rendant disponible dans le rappel.
5. Utilisation de try...catch
Avant ES6, on pouvait imiter une portée de bloc avec try...catch :
for (var compteur = 0; compteur < 5; compteur++) {
try {
throw compteur;
} catch (valeur) {
setTimeout(() => console.log(valeur), 0);
}
}
// Affiche 0, 1, 2, 3, 4
Le bloc catch crée une portée limitée, préservant la valeur courante.