Retour au cours

frontend / javascript

Fonctions, closures et portée

Leçon 21 exercice

Explication

Ce que vous allez apprendre

  • Écrire une fonction avec les trois syntaxes courantes (déclaration, expression, arrow function)
  • Expliquer pourquoi this se 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 var en passant à let
  • Utiliser call, apply et bind pour contrôler explicitement la valeur de this

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.

SyntaxeHoisted ?this proprearguments
function nom() {}Oui, entièrementOui (dépend de l'appel)Oui
const f = function() {}Non (seule la variable l'est)Oui (dépend de l'appel)Oui
const f = () => {}NonNon (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.

js
// 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.
  • var est function-scoped (piège dans les boucles), let/const sont block-scoped.
  • call/apply/bind permettent de contrôler explicitement la valeur de this.

Exercices pratiques

1 disponible
1

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 à :

js
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.

Résoudre l’exercice →