Les closures sont un concept assez obscur du JavaScript et pourtant, parfois, les utiliser est la seule solution pour résoudre certains problèmes.
Le problème
Commençons par poser la problématique. Imaginez que vous ayez 10 paragraphes, ayant chacun un id du type « chiffrex » où x est un chiffre entre 0 et 9. Par défaut (dans le code HTML de base, donc), le contenu de ces paragraphes est « toto ». Maintenant, une seconde après le chargement de la page, vous voudriez modifier le contenu de ces paragraphes : chaque paragraphe doit valoir le fameux « x » qu’il a dans son id. Vous seriez susceptibles de pondre ça :
Closures
toto
toto
toto
toto
toto
toto
toto
toto
toto
toto
toto
function afficherChiffres() {
for (var i = 0; i < 10; i++)
setTimeout(function(){afficherChiffre(i);}, 1000);
}
function afficherChiffre(i) {
document.getElementById('chiffre' + i).textContent = i;
}
Ce qui est totalement naturel si on n'a jamais été confronté au problème. Oui, un problème, parfaitement, il y en a un. Vous pouvez noter une particularité : j'ai rajouté un 11e paragraphe : chiffre10. Si vous testez ce code, vous noterez qu'il n'y a qu'une seule modification et elle se situe dans le seul paragraphe où l'on n'a rien demandé : le chiffre10.
Vous avez envoyé le bon chiffre, en fait. L'ennui c'est qu'en une seconde il a largement eu le temps d'être modifié car c'est en fait une référence vers la variable i que vous avez envoyé à la fonction dans setTimeout(). Ainsi, durant toute la seconde où setTimeout() a attendu, i a fini son tour de boucle et a donc sa valeur à 10, d'où le problème.
Pour résoudre le problème, nous allons utiliser ce qu'on appelle les closures. Le principe est simple, il s'agit d'enfermer la variable dans un espace de nom différent de celui de la boucle, afin qu'elle ne soit pas modifiée. Pour changer d'espace de nom, c'est simple, il faut créer une fonction. Mais pas n'importe quelle fonction, évidemment, il faut créer une fonction qui s'exécute immédiatement et qui n'a pas de nom (elle ne s'exécute qu'une seule fois et toute seule donc n'a pas besoin de nom).
C'est un principe un peu bizarre, certes, mais malgré ce qu'on peut penser, il est souvent utile. Voici comment on résout le problème (seule la fonction afficherChiffres() est modifiée) :
function afficherChiffres() {
for (var i = 0; i < 10; i++) {
(function() {
var j = i;
setTimeout(function(){afficherChiffre(j);}, 1000);
})();
}
}
Vous pouvez tester, ça fonctionne (sans toucher au 11e paragraphe). En fait, tout le contenu de la boucle est exécuté immédiatement, comme s'il n'y avait pas de fonction. Sauf qu'il y en a une et elle a eu pour effet de créer un nouvel espace de nom comme on le voulait. Ce nouvel espace de nom sera unique pour chaque tour de boucle, donc c'est une variable j différente qui sera créée à chaque fois, ce qui fait qu'elle ne sera jamais modifiée (si dans setTimeout() vous aviez mis afficherChiffre(i);, le problème serait resté le même...).
On peut raccourcir cette écriture, en indiquant un paramètre à la fonction anonyme (paramètre dont la valeur sera donnée dans les parenthèses situées juste après la déclaration de la fonction) :
function afficherChiffres() {
for (var i = 0; i < 10; i++) {
(function(i) {
setTimeout(function(){afficherChiffre(i);}, 1000);
})(i);
}
}
Évidemment, ce paramètre aurait pu garder le nom j, ça aurait marché, mais cette écriture a l'avantage de conserver une sorte de continuité. Si vous savez ce qui se passe derrière, vous n'aurez aucun soucis avec cette écriture qui simplifie un peu la vie. À noter que vous pouvez sans problème encapsuler plusieurs variables de cette façon, en indiquant tout simplement autant de paramètres dans la fonction anonyme... Il ne faut pas oublier que ce n'est qu'une fonction et qu'on peut utiliser sur elle tout ce qu'on sait des fonctions classiques.
2 commentaires
[…] C’est un concept qui peut s’avérer utile dans pas mal de cas et, nativement, le JavaScript ne le gère pas. Il y a bien sûr la possibilité de passer par des variables globales mais ce n’est pas vraiment la meilleure façon de s’en sortir… Il faut en fait utiliser les closures. […]
[…] vous allez pouvoir les rattraper grâce à deux autres tutoriels de votre serviteur), à savoir les closures et plus particulièrement les variables statiques. À noter que je ne rappelle pas ici la façon […]