Une fermeture, ou closure en anglais, est une fonction qui fait utiliser des variables indépendantes (utilisées localement mais définies dans la portée englobante). Autrement dit, ces fonctions se « souviennent » de l'environnement dans lequel elles ont été créées (on dit aussi que la fonction capture son « environnement »).

Portée lexicale

Avec l'exemple suivant :

function init() {
    var nom = "Mozilla"; // nom est une variable locale de init
    function afficheNom() { // afficheNom est une fonction interne de init
        alert(nom); // ici nom est une variable libre (définie dans la fonction parente)
    }
    afficheNom();
};
init();

la fonction init a une variable locale nom et une fonction interne afficheNom. La fonction interne est seulement visible de l'intérieur de init. Contrairement à init, afficheNom ne possède pas de variable locale propre, mais elle utilise la variable nom de 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.

Fermeture

Si on a l'exemple suivant :

function creerFonction() {
  var nom = "Mozilla";
  function afficheNom() {
    alert(nom);
  }
  return afficheNom;
}

var maFonction = creerFonction();
maFonction();

et qu'on exécute ce code, on obtient exactement le même résultat qu'en exécutant l'appel de fonction init() étudié précédemment : le texte "Mozilla" est affiché dans une boite d'alerte. L'intérêt de ce code est qu'une fermeture contenant la fonction afficheNom est renvoyée par 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 creerFonction() a fini son exécution, on peut penser que la variable nom n'est plus accessible. Cependant, le code fonctionne : la variable est donc accessible d'une certaine façon.

La solution est la suivante : maFonction est une fermeture. La fermeture combine la fonction afficheNom et son environnement. 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 une référence à la variable var nom = "Mozilla" qui existait lorsque la fermeture 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

où l'on définit une fonction faireAddition(x) qui a un seul argument x et qui renvoie une fonction anonyme. La fonction anonyme a un seul argument y, et renvoie la somme de x et y.

La fonction faireAddition permet de créer des fermetures qui font la somme de leur argument et d'un nombre fixé. Dans l'exemple ci-dessus, on crée ajout5 et ajout10. Elles partagent la même fonction, mais des environnements différents. Dans ajout5, x vaut 5 ; dans ajout10, x vaut 10.

Les fermetures en pratique

On a vu la théorie décrivant les fermetures. Est-ce qu'elles sont utiles pour autant ? Une fermeture 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 fermeture pour tout endroit où on utiliserait un objet et ce avec une seule méthode.

Beaucoup de code JavaScript utilisé sur le Web gère des événements : on définit un comportement, puis on l'attache à un événement déclenché par l'utilisateur (tel un clic ou une frappe clavier). Notre code est généralement une fonction de rappel (ou callback) exécutée en réponse à 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 de l'élément body, ce changement étant répercuté aux 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 fermetures qui peuvent, 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> 

Émuler des méthodes privées avec des fermetures

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 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 compteurPrive = 0;
  function changeValeur(val) {
    compteurPrive += val;
  }
  return {
    increment: function() {
      changeValeur(1);
    },
    decrement: function() {
      changeValeur(-1);
    },
    valeur: function() {
      return compteurPrive;
    }
  };   
})();

console.log(compteur.valeur()); /* Affiche 0 */
compteur.increment();
compteur.increment();
console.log(compteur.valeur()); /* Affiche 2 */
compteur.decrement();
console.log(compteur.valeur()); /* Affiche 1 */

Il y a beaucoup de différences par rapport aux exemples précédents. Au lieu de retourner une simple fonction, on retourne un objet anonyme qui contient 3 fonctions. Et ces 3 fonctions partagent le même environnement. L'objet retourné est affecté à la variable compteur, et les 3 fonctions sont alors accessibles sous les noms compteur.increment, compteur.decrement, et compteur.valeur.

L'environnement partagé vient du corps de la fonction anonyme qui est exécutée dès sa définition complète. L'environnement en question contient deux éléments privés : une variable compteurPrive et une fonction changeValeur. Aucun de ces deux éléments ne peut être utilisé en dehors de la fonction anonyme ; seules les trois fonctions renvoyées par la fonction anonyme sont publiques.

Ces trois fonctions publiques sont des fermetures qui partagent le même environnement. Grâce à la portée lexicale, chacune a accès à compteurPrive 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 creerCompteur = function() {
  var compteurPrive = 0;
  function changeValeur(val) {
    compteurPrive += val;
  }
  return {
    increment: function() {
      changeValeur(1);
    },
    decrement: function() {
      changeValeur(-1);
    },
    valeur: function() {
      return compteurPrive;
    }
  };   
};

var compteur1 = creerCompteur();
var compteur2 = creerCompteur();
alert(compteur1.valeur()); /* Affiche 0 */
compteur1.increment();
compteur1.increment();
alert(compteur1.valeur()); /* Affiche 2 */
compteur1.decrement();
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. Un nouvel environnement est instancié à chaque appel creerCompteur().

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 fermetures et les boucles : attention au mélange

Avant que le mot clé let ne soit introduit avec ECMAScript 6, un problème se posait fréquemment 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 preparerAide() {
  var texteAide = [
      {'id': 'email', 'aide': 'Votre adresse e-mail'},
      {'id': 'nom', 'aide': 'Vos prénom et nom'},
      {'id': 'âge', 'aide': 'Votre âge (plus de 16 ans requis)'}
    ];

  for (var i = 0; i < texteAide.length; i++) {
    var item = texteAide[i];
    document.getElementById(item.id).onfocus = function() {
      afficherAide(item.aide);
    }
  }
}

preparerAide();

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, c'est toujours le message d'aide concernant l'âge qui s'affiche.

La cause de ce problème est que les fonctions attachées à onfocus sont des fermetures qui partagent le même environnement. À chaque itération de boucle, l'environnement de la fermeture créée contient une référence sur la même instance de la variable item. Ainsi, lorsque la fonction de rappel de onfocus est exécutée, la boucle a déjà été effectuée entièrement, et la variable item partagée par les trois fermetures pointe sur le dernier élément de texteAide.

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

function afficheAide(aide) {
  document.getElementById('aide').innerHTML = aide;
}

function creerCallbackAide(aide) {
  return function() {
    afficheAide(aide);
  };
}

function prepareAide() {
  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 = creerCallbackAide(item.aide);
  }
}

prepareAide(); 

Les performances et les fermetures

Il est mal avisé de créer des fonctions imbriquées et des fermetures sans utilité. En effet, cela peut dégrader les performances 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. Il pourrait être mieux écrit ainsi :

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

Cependant, redéfinir le prototype est déconseillé, donc encore meilleur serait d'ajouter les 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 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

 Contributeurs à cette page : SphinxKnight, Mongenet, opii93, bassam, DeepFriedSeagull, cosmith, teoli, Florentsuc, zanz
 Dernière mise à jour par : SphinxKnight,