Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Principe des fermetures illustré par les boucles
Un billet blog de yahiko

Le , par yahiko

0PARTAGES

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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
1
2
3
4
5
for (var i = 0; i < 10; i++) { 
    setTimeout(function (value) { 
        alert(value); 
    }.bind(this, i), 1000); 
}
Exemple 5

Une erreur dans cette actualité ? Signalez-le nous !