Pourquoi votre code JavaScript se comporte-t-il comme un téléphone arabe ?
Imaginez : vous écrivez une simple boucle, vous ajoutez des gestionnaires d'événements sur des boutons, et soudain — bam ! — tous les boutons affichent le même nombre. Familier ? Si vous avez déjà été confronté aux fermetures dans une boucle JavaScript, vous connaissez ce moment de dissonance cognitive. Vous êtes sûr que le code est correct, mais le navigateur insiste obstinément : "Non, mon ami, tu as fait une erreur".
Sur Stack Overflow, cette question a recueilli des centaines de milliers de vues. Et ce n'est pas surprenant : la capture de variables de boucle dans une fermeture est l'un des pièges les plus fréquents pour les débutants et même les développeurs expérimentés. Comprenons pourquoi cela se produit et, surtout, comment y remédier en tenant compte des performances.
Le problème classique : var et l'asynchronisme
Jetez un œil à ce code inoffensif :
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); // Qu'affichera-t-il ? }, 100);}Si vous avez répondu "0, 1, 2, 3, 4" — félicitations, vous êtes un optimiste. La réalité est cruelle : la console affichera 5, 5, 5, 5, 5. Pourquoi ? Parce que var a une portée fonctionnelle, et non une portée de bloc. Au moment où setTimeout s'exécute, la boucle est déjà terminée et i est égal à 5. Les cinq fermetures référencent la même variable.
C'est un exemple classique du problème des fermetures dans une boucle JavaScript. Et la solution est évidente : il faut créer une nouvelle portée pour chaque itération.
Solution n°1 : IIFE — la vieille béquille fiable
Avant l'arrivée d'ES6, les développeurs utilisaient les fonctions immédiatement invoquées (IIFE) :
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i);}Ici, j est une copie de i au moment de l'appel de l'IIFE. Chaque fermeture capture sa propre valeur. Ça marche ? Oui. C'est joli ? Pas vraiment. Du point de vue de l'optimisation des fermetures, cela crée une fonction supplémentaire à chaque itération, ce qui peut avoir un impact sur la mémoire si la boucle est énorme.
Solution n°2 : let — le sauveur d'ES6
Avec l'apparition de let, le problème a été résolu élégamment :
for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); // 0, 1, 2, 3, 4 }, 100);}let crée une nouvelle liaison pour chaque itération de la boucle. C'est un comportement intégré, spécifié dans la spécification ECMAScript. Chaque fermeture reçoit sa propre copie de i. Performances ? Excellent — le moteur optimise cela au niveau de la compilation.
Cependant, il y a une nuance : si vous utilisez let à l'intérieur de la boucle, mais en dehors du bloc for, le comportement peut différer. Gardez toujours let à l'intérieur de l'en-tête de la boucle.
Solution n°3 : bind — l'approche fonctionnelle
La méthode bind permet de fixer les arguments :
for (var i = 0; i < 5; i++) { setTimeout(function(j) { console.log(j); }.bind(null, i), 100);}Ici, bind crée une nouvelle fonction avec un argument prédéfini. Cela fonctionne, mais crée une fonction supplémentaire à chaque itération. En termes de performances, bind est légèrement plus lent que l'IIFE en raison des vérifications internes.
Solution n°4 : forEach et autres méthodes de tableaux
Parfois, la meilleure façon d'éviter le problème est de ne pas utiliser for :
[0, 1, 2, 3, 4].forEach(function(i) { setTimeout(function() { console.log(i); }, 100);});forEach crée une nouvelle portée pour chaque itération via le callback. Cela non seulement résout