mozilla
Vos résultats de recherche

    Closures (Fermetures)

    Les fermetures, ou closures en anglais, sont des fonctions qui utilisent des variables libres.

    Autrement dit, les variables de la fonction parente de la fermeture restent liées à la portée parente.

    Avec l'exemple suivant :

    function init() {
        var nom = "Mozilla"; // nom est une variable locale créée par init
        function afficheNom() { // afficheNom() est une fonction interne, une closure
            alert(nom); // afficheNom() utilise une variable de la fonction parente
        }
        afficheNom();    
    }
    init();

    init() crée une variable locale nom et une fonction intitulée afficheNom().  La fonction afficheNom() est une fonction interne (une fermeture), définie à l'intérieur de init(), elle est seulement disponible depuis l'intérieur de init(). Contrairement à init(), afficheNom() ne possède pas de variable locale propre, elle réutilise la variable nom déclarée dans la fonction parente.

    Vous pouvez exécuter le code sur cette page pour voir son fonctionnement. On a ici un exemple de portée lexicale : en JavaScript, la portée d'une variable est définie par son emplacement dans le code source (elle apparaît de façon lexicale), les fonctions imbriquées ont ainsi accès aux variables déclarées dans les portées parentes.

    Si on a l'exemple suivant :

    function créerFonction() {
      var nom = "Mozilla";
      function afficheNom() {
        alert(nom);
      }
      return afficheNom;
    }
    
    var maFonction = créerFonction();
    maFonction();
    

    et qu'on exécute ce code, on obtiendra exactement le même résultat qu'en exécutant la fonction init() étudiée précédemment : le texte "Mozilla" sera affiché dans une fenêtre d'alerte. L'intérêt de ce code est que la fonction afficheNom() a été renvoyée depuis la fonction parente avant d'être exécutée.

    Le code continue à fonctionner, ce qui peut paraître contre-intuitif au regard de la syntaxe utilisée. Normalement, les variables locales d'une fonction n'existent que pendant l'exécution d'une fonction. Une fois que créerFonction() aura fini son exécution, on peut penser que la variable nom ne sera plus accessible. Cependant, le code continue à fonctionner : la variable est donc accessible d'une certaine façon.

    La solution est la suivante : créerFonction est devenue une closure. Une fermeture, ou closure, est un objet spécial qui combine deux éléments : une fonction et l'environnement dans lequel la fonction a été créée. L'environnement est composé de toutes les variables locales de la portée présente lorsque la fermeture a été créée. Ici maFonction est une fermeture qui contient la fonction afficheNom et la chaîne de caractères "Mozilla" qui existait lorsque la closure a été créée.

    Voici un exemple supplémentaire : une fonction faireAddition :

    function faireAddition(x) {
      return function(y) {
        return x + y;
      };
    }
    
    var ajout5 = faireAddition(5);
    var ajout10 = faireAddition(10);
    
    console.log(ajout5(2));  // 7
    console.log(ajout10(2)); // 12
    

    Ici, on définit une fonction faireAddition(x) qui utilise un seul argument x et qui renvoie une nouvelle fonction. La fonction renvoyée utilise un seul argument y, et renvoie la somme de x et y.

    faireAddition permet de créer d'autres fonctions (qui font la somme de leur argument et d'un nombre fixé). Dans l'exemple ci-dessus, on crée deux fonctions, la première qui ajoute 5 à l'argument et la deuxième qui ajoute 10.

    ajout5 et ajout10 sont des fermetures. Ils partagent la même définition de fonction mais des environnements différents. Dans l'environnement de ajout5 x vaut 5. Pour ajout10 x vaut10.

    Les fermetures en pratique

    On a vu la théorie décrivant les fermetures. Est-ce qu'elles sont utiles pour autant ? Une closure permet d'associer des données (l'environnement) avec une fonction qui agit sur ces données. On peut faire un parallèle avec la programmation orientée objet car les objets permettent d'associer des données (les propriétés) avec des méthodes.

    Ainsi, on peut utiliser une closure pour tout endroit où on utiliserait un objet et ce avec une seule méthode.

    Pourquoi faire cela ? Les situations sont nombreuses, la plupart du code JavaScript utilisé sur le Web permet de gérer des événements : on définit un comportement, attaché à un événement qui sera déclenché par l'utilisateur (un clic ou une frappe clavier). Le code utilisé est généralement rattaché à une fonction de rappel (ou callback) déclenchée par l'événement.

    Voici un exemple concret : si on souhaite ajouter des boutons à une page afin d'ajuster la taille du texte, on pourrait définir la taille de police de l'élément body en pixels et celles des autres éléments relativement à cette première taille grâce à l'unité em :

    body {
      font-family: Helvetica, Arial, sans-serif;
      font-size: 12px;
    }
    
    h1 {
      font-size: 1.5em;
    }
    h2 {
      font-size: 1.2em;
    }
    

    Les boutons vont ensuite changer la taille de la police pour l'élément body, ce changement sera répercuté pour les autres éléments grâce aux unités relatives.

    Voici le code JavaScript qui correspond :

    function fabriqueRedimensionneur(taille) {
      return function() {
        document.body.style.fontSize = taille + 'px';
      };
    }
    
    var taille12 = fabriqueRedimensionneur(12);
    var taille14 = fabriqueRedimensionneur(14);
    var taille16 = fabriqueRedimensionneur(16);
    

    taille12, taille14, et taille16 sont désormais des fonctions qui pourront, respectivement, redimensionner le texte de l'élément body à 12, 14, ou 16 pixels. On peut les attacher aux boutons de la façon suivantes :

    document.getElementById('taille-12').onclick = taille12;
    document.getElementById('taille-14').onclick = taille14;
    document.getElementById('taille-16').onclick = taille16;
    
    <a href="#" id="taille-12">12</a>
    <a href="#" id="taille-14">14</a>
    <a href="#" id="taille-16">16</a> 
    

    Voir dans JSFiddle

    Utiliser les closures pour émuler des méthodes privées

    Certains langages de programmation, comme Java, permettent d'avoir des méthodes privées, c'est-à-dire qu'on ne peut les utiliser qu'au sein de la même classe.

    JavaScript ne permet pas de faire cela de façon native. En revanche, on peut émuler ce comportement grâce aux fermetures. Les méthodes privées ne sont pas seulement utiles en termes de restriction d'accès au code, elles permettent également de gérer un espace de nom (namespace) global qui isole les méthodes secondaires de l'interface publique du code ainsi rendu plus propre.

    Voici comment on peut définir une fonction publique accédant à des fonctions et des variables privées en utilisant des fermetures. Cette façon de procéder est également connue comme le patron de conception module :

    var compteur = (function() {
      var compteurPrivé = 0;
      function changeValeur(val) {
        compteurPrivé += val;
      }
      return {
        incrément: function() {
          changeValeur(1);
        },
        décrément: function() {
          changeValeur(-1);
        },
        valeur: function() {
          return compteurPrivé;
        }
      };   
    })();
    
    alert(compteur.valeur()); /* Affiche 0 */
    compteur.incrément();
    compteur.incrément();
    alert(compteur.valeur()); /* Affiche 2 */
    Compteur.décrément();
    alert(compteur.valeur()); /* Affiche 1 */
    

    Il y a beaucoup de différences par rapport aux exemples précédents : contrairement à ces exemples où chaque closure possèdait son propre environnement, on crée ici un seul environnement partagé par trois fonctions : compteur.incrément, compteur.décrément, et compteur.valeur.

    L'environnement qui est partagé est créé au sein du corps de la fonction anonyme qui est exécutée dès qu'elle a été définie. L'environnement en question contient deux éléments privés : une variable compteurPrivé et une fonction changeValeur. Aucun de ces deux éléments ne peut être utilisé en dehors de la fonction anonyme : seules les trois fonctions publiques renvoyées par la fonction anonyme permettent de les utiliser indirectement.

    Ces trois fonctions publiques sont des fermetures qui partagent le même environnement. Grâce à la portée lexicale, chacune a accès à compteurPrivé et à changeValeur.

    On remarquera qu'on définit une fonction anonyme qui crée un compteur puis qu'on l'appelle immédiatement pour assigner le résultat à la variable Compteur. On pourrait stocker cette fonction dans une variable puis l'appeler plusieurs fois afin de créer plusieurs compteurs.

    var créerCompteur = function() {
      var compteurPrivé = 0;
      function changeValeur(val) {
        compteurPrivé += val;
      }
      return {
        incrément: function() {
          changeValeur(1);
        },
        décrément: function() {
          changeValeur(-1);
        },
        valeur: function() {
          return compteurPrivé;
        }
      };   
    };
    
    var compteur1 = créerCompteur();
    var compteur2 = créerCompteur();
    alert(compteur1.valeur()); /* Affiche 0 */
    compteur1.incrément();
    compteur1.incrément();
    alert(compteur1.valeur()); /* Affiche 2 */
    compteur1.décrément();
    alert(compteur1.valeur()); /* Affiche 1 */
    alert(compteur2.valeur()); /* Affiche 0 */
    

    Ici on peut voir que chacun des deux compteurs est indépendant de l'autre. L'environnement utilisé pendant l'appel à créerCompteur() est bien distinct à chaque fois. C'est pourquoi la variable compteurPrivé contient une instance différente pour chaque.

    L'utilisation de fermetures permet ainsi de bénéficier de certains concepts liés à la programmation orientée objet comme l'encapsulation et la dissimulation de données.

    Les closures et les boucles : attention au mélange

    Avant que le mot clé let ne soit introduit avec JavaScript 1.7, un problème fréquent pouvait se poser lorsqu'on manipulait des fermetures au sein d'une boucle. Par exemple :

    <p id="aide">Des aides seront affichées ici</p>
    <p>E-mail : <input type="text" id="email" name="email"></p>
    <p>Nom : <input type="text" id="nom" name="nom"></p>
    <p>Âge : <input type="text" id="âge" name="âge"></p>
    
    function afficherAide(aide) {
      document.getElementById('aide').innerHTML = aide;
    }
    
    function paramAide() {
      var texteAide = [
          {'id': 'email', 'aide': 'Votre adresse e-mail'},
          {'id': 'nom', 'aide': 'Votre prénom et nom'},
          {'id': 'âge', 'aide': 'Your age (you must be over 16)'}
        ];
    
      for (var i = 0; i < texteAide.length; i++) {
        var item = texteAide[i];
        document.getElementById(item.id).onfocus = function() {
          afficherAide(item.aide);
        }
      }
    }
    
    paramAide(); 
    

    Voir dans JSFiddle

    Le tableau texteAide permet de définir les textes d'informations, chaque texte étant associé à un identifiant de l'élément input du document. La boucle parcourt ces définitions et attache un gestionnaire d'événement onfocus à chaque élément avec l'aide qui correspond.

    Lorsqu'on essaie ce code, on s'aperçoit qu'il ne fonctionne pas exactement comme on le souhaitait : en effet, quelque soit le champ sur lequel on se situe, le message d'aide concernant l'âge est le seul qui s'affiche.

    La raison à ce problème est que les fonctions attachées aux gestionnaires d'événements sont des closures et que l'environnement qui leur est rattaché est le même pour les trois : il provient de la portée de la fonction paramAide. On a donc trois fermetures créées mais qui partagent le même environnement. Ainsi, lorsque la fonction de rappel est exécutée, la boucle a déjà été effectuée entièrement et la variable item (partagée par les trois closures) pointe alors vers le dernier élément de la liste texteAide.

    Une solution consiste à utiliser plus de closures et à appliquer une fabrique de fonction comme on a vu précédemment :

    function afficheAide(aide) {
      document.getElementById('aide').innerHTML = aide;
    }
    
    function créerCallbackAide(aide) {
      return function() {
        afficheAide(aide);
      };
    }
    
    function paramAide() {
      var texteAide = [
          {'id': 'email', 'aide': 'Votre adresse e-mail'},
          {'id': 'nom', 'aide': 'Votre prénom et nom'},
          {'id': 'âge', 'aide': 'Your age (you must be over 16)'}
        ];
    
      for (var i = 0; i < texteAide.length; i++) {
        var item = texteAide[i];
        document.getElementById(item.id).onfocus = créerCallbackAide(item.aide);
      }
    }
    
    paramAide(); 
    

    Voir dans JSFiddle

    Ici le fonctionnement est celui attendu. Utiliser la fonction créerCallbackAide permet de créer un nouvel environnement pour chacun des gestionnaires d'événements qui se réfèrent à un élément différent du tableau texteAide.

    Les performances et les closures

    Il est mal avisé de créer des fonctions imbriquées et des closures si cela n'est pas nécessaire pour une certaine tâche. En effet, cela peut rendre le script moins performant en termes de vitesse d'exécution et de consommation de mémoire.

    Quand, par exemple, on crée un nouvel objet, les méthodes devraient être associées au prototype de l'objet et non pas définies dans le constructeur de l'objet. De cette façon, on évite que les méthodes soient réassignées à chaque fois qu'un nouvel objet est créé.

    Voici un exemple de la mauvaise façon de procéder :

    function MonObjet(nom, message) {
      this.nom = nom.toString();
      this.message = message.toString();
      this.getNom = function() {
        return this.nom;
      };
    
      this.getMessage = function() {
        return this.message;
      };
    }
    

    Le fragment de code précédent ne tire pas partie des avantages des fermetures. Une meilleure écriture, correspondante, pourrait être la suivante :

    function MonObjet(nom, message) {
      this.nom = nom.toString();
      this.message = message.toString();
    }
    MonObjet.prototype = {
      getNom: function() {
        return this.nom;
      },
      getMessage: function() {
        return this.message;
      }
    };
    

    Ou encore, sans redéfinir le prototype mais plutôt en y ajoutant de nouvelles méthodes :

    function MonObjet(nom, message) {
      this.nom = nom.toString();
      this.message = message.toString();
    }
    MonObjet.prototype.getNom = function() {
      return this.nom;
    };
    MonObjet.prototype.getMessage = function() {
      return this.message;
    };
    

    Les deux derniers exemples permettent de voir que le prototype hérité est partagé par tous les objets construits et que les méthodes définitions n'ont pas besoin d'être reconstruites pour chaque création d'objet. Veuillez consulter la page sur le modèle objet JavaScript en détails pour plus d'informations.

     

    Étiquettes et contributeurs liés au document

    Contributors to this page: cosmith, zanz, SphinxKnight, DeepFriedSeagull, Florentsuc, teoli
    Dernière mise à jour par : SphinxKnight,
    Masquer la barre latérale