IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

Le Blog d'un Ninja codeur

[Actualité] [JS] Principe des fermetures illustré par les boucles

Noter ce billet
par , 05/02/2015 à 14h19 (1748 Affichages)
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
1
2
3
4
5
for (var i = 0; i < 10; i++) {
    setTimeout(function (value) {
        alert(value);
    }.bind(this, i), 1000);
}
Exemple 5

Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Viadeo Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Twitter Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Google Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Facebook Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Digg Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Delicious Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog MySpace Envoyer le billet « [JS] Principe des fermetures illustré par les boucles » dans le blog Yahoo

Mis à jour 10/02/2015 à 12h49 par Bovino

Catégories
Développement , Javascript , Développement Web

Commentaires

  1. Avatar de danielhagnoul
    • |
    • permalink
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    for ( var i = 0; i < 10; i++ ) {
        setTimeout( function( value ){
            console.log( value );
        }.bind( this, i ), 1000 );
    }
  2. Avatar de yahiko
    • |
    • permalink
    Citation Envoyé par danielhagnoul
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    for ( var i = 0; i < 10; i++ ) {
        setTimeout( function( value ){
            console.log( value );
        }.bind( this, i ), 1000 );
    }
    Un copier-coller du code de l'exemple 3 (assez daté) qui s'est propagé au reste. Merci pour le signalement.
  3. Avatar de SylvainPV
    • |
    • permalink
    Array.prototype.forEach est très utile dans ces cas précis
  4. Avatar de yahiko
    • |
    • permalink
    Citation Envoyé par SylvainPV
    Array.prototype.forEach est très utile dans ces cas précis
    On est d'accord