Définition et Mécanisme des Fermetures
En JavaScript, une fermeture (closure) est la combinaison d'une fonction et de l'environnement lexical dans lequel elle a été déclarée. Cet environnement comprend les variables locales qui étaient dans la portée au moment de la création de la fermeture. Lorsqu'une fonction interne est renvoyée ou exposée par une fonction externe, elle conserve une référence aux variables de la fonction parente, empêchant ainsi leur destruction par le ramasse-miettes (garbage collector) même après la fin de l'exécution de la fonction externe.
Modèles Courants de Fermetures
Les fermetures sont souvent utilisées pour créer des générateurs d'état ou pour gérer des opérations asynchrones tout en préservant le contexte.
// Générateur de compteur avec un pas personnalisable
function genererCompteurPas(pas) {
let valeurActuelle = 0;
return function() {
valeurActuelle += pas;
return valeurActuelle;
};
}
const compteurParTrois = genererCompteurPas(3);
console.log(compteurParTrois()); // Affiche: 3
console.log(compteurParTrois()); // Affiche: 6
console.log(compteurParTrois()); // Affiche: 9
// Gestion d'un délai avec conservation du contexte
function notificationDifferee(message, delai) {
setTimeout(function() {
console.log(`Notification: ${message}`);
}, delai);
}
notificationDifferee('Traitement terminé', 1500);
Utilité et Avantages
Les fermetures permettent de prolonger la durée de vie des variables locales et d'implémenter l'encapsulation des données. Elles offrent la possibilité de lire et modifier des données privées depuis l'extérieur de la fonction qui les a déclarées.
function creerGestionnaireEtat(valeurInitiale) {
let etat = valeurInitiale;
return {
incrementer: function() {
etat++;
console.log(`État actuel: ${etat}`);
},
decrementer: function() {
etat--;
console.log(`État actuel: ${etat}`);
}
};
}
const gestionnaire = creerGestionnaireEtat(10);
gestionnaire.incrementer(); // État actuel: 11
gestionnaire.decrementer(); // État actuel: 10
Cycle de Vie d'une Fermeture
La création d'une fermeture intervient lors de la définition de la fonction interne (au moment de la création du contexte d'exécution, grâce au hissage ou hoisting pour les déclarations classiques). Sa destruction se produit uniquement lorsque la fonction interne n'est plus référencée et devient éligible au ramasce-miettes.
function initialiserContexte() {
let compteur = 5;
// La fermeture est créée ici lors de la définition de la fonction
function afficherEtIncrementer() {
compteur++;
console.log(compteur);
}
return afficherEtIncrementer;
}
let executer = initialiserContexte();
executer(); // 6
executer(); // 7
// Destruction de la référence pour libérer la mémoire
executer = null;
Applications Pratiques : Le Modèle de Module
Avant l'introduction des modules ES6, les fermetures étaient le mécanisme standard pour créer des modules JavaScript avec des données privées et des API publiques.
Approche par Fonction de Fabrique
function modulePanier() {
// Données privées
let articles = [];
// Fonctions internes
function ajouter(article) {
articles.push(article);
console.log(`${article} ajouté au panier.`);
}
function afficherTotal() {
console.log(`Nombre total d'articles: ${articles.length}`);
}
// Exposition de l'API publique
return {
ajouter: ajouter,
afficherTotal: afficherTotal
};
}
const monPanier = modulePanier();
monPanier.ajouter('Laptop');
monPanier.afficherTotal();
Approche par IIFE (Expression de Fonction Immédiatement Invoquée)
const ModuleAuthentification = (function() {
let utilisateurConnecte = null;
function connecter(nom) {
utilisateurConnecte = nom;
console.log(`Utilisateur ${nom} connecté.`);
}
function deconnecter() {
utilisateurConnecte = null;
console.log('Déconnexion réussie.');
}
return {
connecter: connecter,
deconnecter: deconnecter
};
})();
ModuleAuthentification.connecter('Alice');
ModuleAuthentification.deconnecter();
Inconvénients et Optimisation Mémoire
L'utilisation excessive de fermetures peut entraîner une consommation mémoire élevée, car les variables capturées ne sont pas libérées tant que la fermeture existe. Cela peut provoquer des fuites de mémoire si les références ne sont pas correctement nettoyées.
function allouerMemoireLourde() {
const grosTableau = new Array(100000).fill('données_sensibles');
function verifierTaille() {
console.log(`Taille du tableau: ${grosTableau.length}`);
}
return verifierTaille;
}
let tache = allouerMemoireLourde();
tache();
// Libération explicite de la closure pour permettre le garbage collection
tache = null;
Gestion du Contexte this dans les Fermetures
Un défi classique lors de l'utilisation de fermetures dans les méthodes d'objet est la perte du contexte this. La fonction interne ne possède pas son propre this et se rabat sur le contexte global (ou undefined en mode strict).
// Problème de perte de contexte
const contexteGlobal = "Global";
const objetCible = {
contexteGlobal: "Local",
obtenirFonction: function() {
return function() {
return this.contexteGlobal;
};
}
};
console.log(objetCible.obtenirFonction()()); // Affiche "Global" (ou undefined)
// Solution classique : alias de `this`
const objetCible2 = {
nom: "Objet Local",
obtenirFonction: function() {
const self = this; // Capture du contexte
return function() {
return self.nom;
};
}
};
console.log(objetCible2.obtenirFonction()()); // Affiche "Objet Local"
// Solution moderne : fonction fléchée
const objetCible3 = {
nom: "Objet Moderne",
obtenirFonction: function() {
// Les fonctions fléchées capturent le `this` de leur portée englobante
return () => this.nom;
}
};
console.log(objetCible3.obtenirFonction()()); // Affiche "Objet Moderne"