Developpez.com - Rubrique Dév. Web

Le Club des Développeurs et IT Pro

Principe des fermetures illustré par les boucles

Un billet blog de yahiko

Le 10/02/2015, par yahiko, Rédacteur/Modérateur
Les fermetures (closure en anglais), ne sont pas une notion toujours bien comprise par les débutants (et pas seulement).

Grosso modo, une fermeture capture ou rattache les variables libres (autrement dit, extérieures à la fermeture) à son propre contexte d'exécution. En langage C, les fonctions ne créent pas de fermeture, une fonction n'ayant accès qu'à son contexte local, ses paramètres et les variables globales. Ce n'est pas le cas en JavaScript puisqu'une fonction peut être imbriquée dans une autre (notamment les fameuses fonctions anonymes) et donc peuvent se voir rattacher des variables de la fonction englobante.

C'est un mécanisme simple mais très puissant en ce qui concerne la programmation dite fonctionnelle. Dans le cadre de JavaScript, les fermetures peuvent cependant être la source de quelques petits problèmes.

Considérons le code suivant :
Code javascript :
1
2
3
4
5
for (var i = 0; i < 10; i++) { 
    setTimeout(function () { 
        alert(i); 
    }, 1000); 
}
Exemple 1

Ici, on devine que l'intention est d'afficher "0", "1", ..., "9" (l'ordre d'apparition n'est pas fondamentalement important). Mais ce n'est pas ce qu'il se produit. Ce code n'affiche que des "10".

La variable i est rattachée au contexte global (window). La fermeture a donc capturé cette variable window.i (et non les valeurs).
Au moment où la fonction anonyme est exécutée (dix fois), la variable i vaut 10.

Pour remédier au problème, il faut que la fermeture capture une variable i qui aura la bonne valeur au moment de l'exécution de la fonction anonyme. D'où la possibilité de créer une fonction intermédiaire comme ce qui suit :
Code javascript :
1
2
3
4
5
6
7
8
9
function fct(i) { 
    setTimeout(function () { 
        alert(i); 
    }, 1000); 
} 
  
for (var i = 0; i < 10; i++) { 
    fct(i); 
}
Exemple 2

La fonction fct créé un nouveau contexte d'exécution (et donc une nouvelle variable i) à chaque appel, ce qui implique que la fermeture capturera une variable i ayant la valeur adéquate. Cet exemple illustre plutôt bien le fait qu'une fermeture capture des variables et non des valeurs.

Une autre possibilité pour contourner le problème de récupération de la variable d'itération dans une boucle, certains développeurs Web préconisent d'associer cette variable d'itération à une instance du DOM...
Code javascript :
1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) { 
  var div = document.createElement('div'); 
  div.innerHTML = 'div ' + i; 
  div.indice = i; 
  div.onclick = function() { alert(this.indice) } 
  document.body.appendChild(div); 
}
Exemple 3

Il n'est pas recommandé d'ajouter arbitrairement une nouvelle propriété à un objet, car cela pose des problèmes de maintenabilité du code. De plus, contrairement à la méthode précédente avec la création d'une fonction intermédiaire permettant à la fermeture de capturer une variable par valeur, cette solution n'est pas généralisable à moins de faire appel à la primitive bind() pour "forcer" le contexte d'exécution.

Si nous voulions généraliser en utilisant une instance d'objet pour la mémorisation de l'indice d'itération, cela pourrait donner ceci :

Code javascript :
1
2
3
4
5
6
for (var i = 0; i < 10; i++) { 
    var obj = { indice: i }; 
    setTimeout(function () { 
        alert(obj.indice); 
    }, 1000); 
}
Exemple 4

Ce code n'affiche que la valeur "9" car contrairement à ce que le bloc à l'intérieur du for pourrait le laisser penser, la variable obj est une variable globale et non locale. Il n'y a donc pas de nouvelle variable créée à chaque appel, et par conséquent la fermeture ne capture qu'une seule variable qui a la valeur "9" au moment où la fonction anonyme est appelée.

Cette instance d'objet dans l'exemple 4 n'a donc strictement rien apporté car en fait l'exemple 3 que préconise certains a la spécificité de créer une instance par itération, et que cette instance est sélectionnée manuellement par l'utilisateur, et non de façon programmée. Difficilement généralisable donc comme je le mentionnais plus haut.

Une des façons les plus élégantes avec la norme ECMAScript 5, c'est d'utiliser la primitive bind() qui nous permet de faire l'économie de la création d'une propriété sur un objet ou contexte existant tout en nous évitant également de définir une fonction intermédiaire (même si c'est ce que réalise bind() en interne).

Code javascript :
1
2
3
4
5
for (var i = 0; i < 10; i++) { 
    setTimeout(function (value) { 
        alert(value); 
    }.bind(this, i), 1000); 
}
Exemple 5
  Billet blog