frontend / javascript
Fonctions, closures et portée
Explication
Ce que vous allez apprendre
- Écrire une fonction avec les trois syntaxes courantes (déclaration, expression, arrow function)
- Expliquer pourquoi
thisse comporte différemment dans une fonction classique et une arrow function - Définir ce qu'est une closure et dire à quoi elle sert concrètement
- Corriger le piège classique des closures dans une boucle
varen passant àlet - Utiliser
call,applyetbindpour contrôler explicitement la valeur dethis
Dans quel contexte ?
Un développeur reçoit un bug report : sur la page d'administration, un bouton "Supprimer" dans une liste générée dynamiquement supprime toujours le dernier élément de la liste, quel que soit le bouton cliqué. En creusant le code, il trouve une boucle for (var i = 0; ...) qui attache un gestionnaire de clic à chaque bouton avec i capturé dans une closure. Le correctif tient en un mot : remplacer var par let. Comprendre pourquoi demande de bien saisir comment une closure capture son environnement.
1. Une fonction, c'est aussi une valeur
Une fonction est un bloc de code réutilisable, mais en JavaScript c'est surtout une valeur comme une autre : elle peut être stockée dans une variable, passée en argument, ou retournée par une autre fonction. Cette flexibilité porte un nom, "first-class functions", et elle est au cœur du style JavaScript idiomatique.
2. Trois syntaxes, un même résultat... presque
Il existe plusieurs façons d'écrire une fonction — déclaration classique, expression de fonction, arrow function — et elles ne se comportent pas toutes pareil. Avant d'entrer dans les nuances, retenez juste qu'elles produisent toutes une valeur appelable.
| Syntaxe | Hoisted ? | this propre | arguments |
|---|---|---|---|
function nom() {} | Oui, entièrement | Oui (dépend de l'appel) | Oui |
const f = function() {} | Non (seule la variable l'est) | Oui (dépend de l'appel) | Oui |
const f = () => {} | Non | Non (hérite du contexte englobant) | Non |
3. La différence qui compte vraiment : this
La nuance la plus importante concerne this. Dans une fonction classique, this dépend de la façon dont la fonction est appelée (son "mode d'appel"), et peut donc changer d'un appel à l'autre. Une arrow function, elle, n'a pas de this propre : elle réutilise celui de son environnement englobant, ce qui évite bien des surprises dans les callbacks d'objets.
Piège fréquent
Définir une méthode d'objet avec une arrow function (incrementerArrow: () => { this.valeur++ }) est une erreur courante : this ne vaudra jamais l'objet, mais le contexte lexical englobant (souvent undefined ou l'objet global). Pour une méthode d'objet, préférez toujours une fonction classique ou la syntaxe raccourcie incrementer() { ... }.
4. Une fonction qui se souvient de son environnement
Une fois cette base posée, voici le concept le plus important de la leçon : la closure. Quand une fonction est définie à l'intérieur d'une autre, elle garde un accès permanent aux variables de son environnement de création, même après que la fonction extérieure a fini de s'exécuter.
5. Ce que les closures rendent possible
Ce mécanisme n'est pas juste théorique : c'est lui qui permet de créer des compteurs privés, des fonctions personnalisées à la volée, ou de vraies variables "privées" avant même l'existence des champs privés de classe. Chaque appel à la fonction externe crée un nouvel environnement, totalement indépendant des autres.
À retenir
Chaque appel à creerCompteur() crée un nouvel environnement de closure : compteur1 et compteur2 ont chacun leur propre variable compte, totalement indépendante. C'est ce qui permet de simuler des instances privées sans utiliser de classe.
6. Le piège classique à connaître
Un exemple illustre bien la puissance — et le danger — des closures : dans une boucle utilisant var, tous les callbacks différés partagent la même variable et affichent la même valeur finale. Avec let, chaque itération crée sa propre variable capturée, ce qui donne le résultat intuitif.
Comprendre ce mécanisme est indispensable avant d'aborder les prototypes et les classes, prochaines leçons, qui reposent eux aussi sur des notions de portée et d'encapsulation.
Commandes & code
Fonctions, closures et portée
Déclarer des fonctions, comprendre le scope et exploiter les closures.
// Declaration de fonction : hoisted (utilisable avant sa definition dans le fichier)
console.log(additionner(2, 3)); // fonctionne grace au hoisting
function additionner(a, b) {
return a + b;
}
// Expression de fonction : PAS hoisted de la meme facon (la variable l'est, pas la valeur)
const soustraire = function (a, b) {
return a - b;
};
// Arrow functions : syntaxe concise, PAS de "this" propre (voir plus bas)
const multiplier = (a, b) => a * b;
const carre = (x) => x ** 2;
const saluer = () => console.log("Bonjour");
// Parametres par defaut et rest parameters
function creerUtilisateur(nom, role = "user", ...permissions) {
return { nom, role, permissions };
}
console.log(creerUtilisateur("Alice", "admin", "read", "write"));
// arguments (uniquement functions classiques, PAS arrow functions)
function sommeVariadique() {
return Array.from(arguments).reduce((acc, n) => acc + n, 0);
}
console.log(sommeVariadique(1, 2, 3, 4));
// this : depend du MODE D'APPEL pour une fonction classique
const compteur = {
valeur: 0,
incrementer: function () {
this.valeur++; // "this" = compteur (appel via compteur.incrementer())
},
incrementerArrow: () => {
this.valeur++; // BUG : "this" ici est le this LEXICAL (pas compteur)
},
};
compteur.incrementer();
console.log(compteur.valeur); // 1
// Closures : une fonction interne capture les variables de son environnement lexical
function creerCompteur() {
let compte = 0;
return {
incrementer: () => ++compte,
obtenir: () => compte,
};
}
const compteur1 = creerCompteur();
const compteur2 = creerCompteur();
compteur1.incrementer();
compteur1.incrementer();
console.log(compteur1.obtenir()); // 2
console.log(compteur2.obtenir()); // 0 -- etat totalement independant
// Closures dans une boucle : piege classique avec var, resolu par let
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var :", i), 0); // affiche 3, 3, 3 (var est function-scoped)
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let :", j), 0); // affiche 0, 1, 2 (let cree un scope par iteration)
}
// IIFE : Immediately Invoked Function Expression -- isoler un scope (pattern historique)
const module = (function () {
let etatPrive = "secret";
return { getEtat: () => etatPrive };
})();
console.log(module.getEtat());
// Fonctions d'ordre superieur : prennent/retournent des fonctions
function creerValidateur(regle) {
return function (valeur) {
return regle(valeur);
};
}
const estMajeur = creerValidateur((age) => age >= 18);
console.log(estMajeur(20));
// bind, call, apply : controler explicitement "this"
function presenter() {
return `Je m'appelle ${this.nom}`;
}
const alice = { nom: "Alice" };
console.log(presenter.call(alice)); // appel immediat avec this = alice
console.log(presenter.apply(alice, [])); // comme call, mais args en tableau
const presenterAlice = presenter.bind(alice); // nouvelle fonction avec this fige
console.log(presenterAlice());Résumé
- Les fonctions déclarées sont hoisted ; les arrow functions n'ont pas leur propre
this. - Une closure capture les variables de son environnement lexical, indépendamment entre chaque appel.
varest function-scoped (piège dans les boucles),let/constsont block-scoped.call/apply/bindpermettent de contrôler explicitement la valeur dethis.
Exercices pratiques
Mission : le bouton Supprimer qui vise toujours la mauvaise ligne
Objectif : Diagnostiquer un bug de closure dans une boucle var, le corriger, et sécuriser une méthode d'objet dont le this est cassé.
Contexte
Sur la page d'administration de Technologik, une liste d'utilisateurs est générée dynamiquement et chaque ligne a un bouton "Supprimer". Un ticket de bug signale que cliquer sur n'importe quel bouton supprime toujours le dernier utilisateur de la liste. Le code fautif ressemble à :
for (var i = 0; i < boutons.length; i++) {
boutons[i].addEventListener("click", function () {
supprimerUtilisateur(utilisateurs[i]);
});
}Un second fichier contient aussi un objet panneauAdmin dont une méthode écrite en arrow function ne met jamais à jour son compteur de clics. Tu dois corriger les deux problèmes et expliquer leur cause commune.