Cours JavaScript et TypeScript · Module 2: Fonctions, tableaux et objets
Closures et portée lexicale
Dans cette leçon7
À Jurassic Park, chaque enclos a sa clôture, et chaque laboratoire une porte à code d'accès. La portée (scope) en JavaScript fonctionne de la même manière : c'est une clôture invisible qui décide quelles variables sont accessibles à un endroit donné du code. Il y a aussi un problème : comment construire un compteur d'observations qu'aucun autre système ne pourra remettre à zéro par erreur ? Une variable globale est accessible à tout le monde. La solution, ce sont les closures (fermetures), c'est-à-dire un dinosaure qui se souvient du code PIN de son enclos même quand le gardien est parti depuis longtemps.
La portée lexicale (Lexical Scope)
JavaScript utilise la portée lexicale : l'accès aux variables dépend de l'endroit où le code a été écrit, et non de l'endroit où il est appelé. Imagine le plan du parc : chaque enclos (fonction) voit la route principale (la portée globale) et son propre territoire, mais ne voit pas l'intérieur des autres enclos.
1// Portée globale - la route principale de Jurassic Park
2const parkName = "Jurassic Park";
3
4function enclosureA() {
5 // Portée de enclosureA - Enclos A
6 const dinosaurA = "Tyrannosaurus Rex";
7
8 console.log(parkName); // "Jurassic Park" - voit la portée globale
9 console.log(dinosaurA); // "Tyrannosaurus Rex" - voit sa propre portée
10
11 function innerFence() {
12 // Portée imbriquée - la clôture intérieure
13 const babyDino = "Baby Rex";
14
15 console.log(parkName); // "Jurassic Park" - voit la portée globale
16 console.log(dinosaurA); // "Tyrannosaurus Rex" - voit la portée parente
17 console.log(babyDino); // "Baby Rex" - voit sa propre portée
18 }
19
20 innerFence();
21 // console.log(babyDino); // ERREUR ! Ne voit pas l'intérieur de innerFence
22}
23
24function enclosureB() {
25 // Portée de enclosureB - Enclos B
26 const dinosaurB = "Triceratops";
27
28 console.log(parkName); // "Jurassic Park" - voit la portée globale
29 // console.log(dinosaurA); // ERREUR ! Ne voit pas l'intérieur de enclosureA
30}La règle est simple : la fonction intérieure voit les variables de la fonction extérieure, mais pas l'inverse. Le dinosaure de l'enclos intérieur voit à travers la clôture vers l'extérieur, alors que le gardien de l'extérieur ne peut pas regarder à l'intérieur à travers les doubles murs. C'est pourquoi innerFence lit dinosaurA sans qu'on lui passe d'argument, alors que enclosureB n'y a pas accès.
Qu'est-ce qu'une closure (fermeture) ?
Une closure est une fonction qui "se souvient" des variables de la portée dans laquelle elle a été créée, même quand cette portée a fini de s'exécuter. Regarde-la à l'œuvre dans un traqueur d'observations :
1function createDinoTracker(species) {
2 // La variable "species" se trouve dans la portée de createDinoTracker
3 let sightings = 0;
4
5 // Cette fonction intérieure crée une CLOSURE
6 // - elle "se souvient" de species et sightings, même après la fin de createDinoTracker
7 return function reportSighting(location) {
8 sightings++;
9 console.log(species + " vu à : " + location + " (observation n°" + sightings + ")");
10 };
11}
12
13// createDinoTracker a terminé, mais la fonction renvoyée SE SOUVIENT des variables
14const trackRex = createDinoTracker("T-Rex");
15const trackRaptor = createDinoTracker("Velociraptor");
16
17trackRex("Secteur A"); // "T-Rex vu à : Secteur A (observation n°1)"
18trackRex("Secteur B"); // "T-Rex vu à : Secteur B (observation n°2)"
19trackRaptor("Laboratoire"); // "Velociraptor vu à : Laboratoire (observation n°1)"
20
21// Chaque closure a SA PROPRE copie des variables !
22// trackRex a son propre sightings = 2, trackRaptor a son propre sightings = 1Chaque appel à createDinoTracker crée une nouvelle portée, les compteurs des deux traqueurs sont donc indépendants. Personne ne peut modifier sightings de l'extérieur, car la variable n'est pas visible hors de la fonction. Le mécanisme se déroule en cinq étapes :
- Tu définis une fonction extérieure avec des variables locales.
- À l'intérieur, tu définis une fonction intérieure qui les utilise.
- La fonction extérieure renvoie la fonction intérieure avec
return. - Tu appelles la fonction extérieure et enregistres le résultat dans une variable.
- Tu appelles la fonction renvoyée, et elle a toujours accès aux variables extérieures.
Les usages pratiques des closures
La confidentialité des données (encapsulation)
Les closures permettent de créer des variables privées, auxquelles personne ne peut accéder de l'extérieur. Comme le système de sécurité du parc, dans lequel les codes d'accès sont cachés dans une armoire fermée à clé :
1function createSecuritySystem(enclosureName) {
2 // Ces variables sont PRIVÉES - inaccessibles de l'extérieur
3 let accessCode = Math.floor(Math.random() * 9000) + 1000;
4 let isLocked = true;
5 const accessLog = [];
6
7 return {
8 unlock(code) {
9 if (code === accessCode) {
10 isLocked = false;
11 accessLog.push("Déverrouillé : " + new Date().toLocaleTimeString());
12 console.log(enclosureName + " - clôture DÉSACTIVÉE");
13 } else {
14 accessLog.push("Tentative échouée : " + new Date().toLocaleTimeString());
15 console.log(enclosureName + " - CODE ERRONÉ !");
16 }
17 },
18 lock() {
19 isLocked = true;
20 accessLog.push("Verrouillé : " + new Date().toLocaleTimeString());
21 console.log(enclosureName + " - clôture ACTIVÉE");
22 },
23 getStatus() {
24 return isLocked ? "FERMÉ" : "OUVERT";
25 },
26 getLog() {
27 return [...accessLog]; // Renvoie une copie, pas l'original
28 }
29 };
30}
31
32const rexSecurity = createSecuritySystem("Enclos du T-Rex");
33// Il n'y a aucun moyen de lire accessCode directement !
34// rexSecurity.accessCode → undefined (variable privée)L'objet renvoyé n'a que quatre méthodes. accessCode, isLocked et accessLog ne sont pas ses propriétés, mais des variables de la closure, donc seules ces méthodes peuvent les modifier. getLog() renvoie une copie du tableau, vider le résultat ne touchera donc pas le vrai journal.
Les fabriques de fonctions (Function Factories)
Les closures permettent de créer des fabriques, c'est-à-dire des fonctions qui produisent d'autres fonctions avec une configuration intégrée. Comme un laboratoire génétique qui crée des dinosaures à partir d'un ADN précis :
1function createDinoFilter(property, value) {
2 // La closure "se souvient" de property et value
3 return function(dinosaur) {
4 return dinosaur[property] === value;
5 };
6}
7
8const isCarnivore = createDinoFilter("diet", "carnivore");
9const isHerbivore = createDinoFilter("diet", "herbivore");
10const isSectorA = createDinoFilter("sector", "A");
11
12const dinosaurs = [
13 { name: "Rex", diet: "carnivore", sector: "A" },
14 { name: "Tricia", diet: "herbivore", sector: "B" },
15 { name: "Blue", diet: "carnivore", sector: "C" }
16];
17
18console.log(dinosaurs.filter(isCarnivore)); // Rex, Blue
19console.log(dinosaurs.filter(isHerbivore)); // Tricia
20console.log(dinosaurs.filter(isSectorA)); // RexTrois filtres sont sortis d'une seule fabrique, et chacun se souvient de sa propre paire property et value. La plus petite fabrique tient en trois lignes :
1function createMultiplier(factor) {
2 return n => n * factor;
3}
4
5const double = createMultiplier(2);
6const triple = createMultiplier(3);
7
8console.log(double(5)); // 10
9console.log(triple(4)); // 12La fonction fléchée se souvient de factor, donc double et triple ont le même code mais une configuration différente.
IIFE - Immediately Invoked Function Expressions
Une IIFE (expression de fonction invoquée immédiatement) est une fonction définie puis exécutée aussitôt. C'est un protocole de sécurité à usage unique : il se lance, fait son travail et disparaît, sans laisser de traces dans la portée globale.
1// Syntaxe d'une IIFE
2(function() {
3 const secretData = "Emplacement du laboratoire caché";
4 console.log("IIFE exécutée ! Données : " + secretData);
5})();
6// secretData n'existe pas dans la portée globale !
7
8// IIFE fléchée
9(() => {
10 const emergencyProtocol = "Évacuation du secteur 7";
11 console.log("Protocole d'urgence activé : " + emergencyProtocol);
12})();
13
14// IIFE avec paramètres
15const dinoReport = (function(parkData) {
16 const processed = parkData.map(d => d.toUpperCase());
17 return processed.join(", ");
18})(["rex", "raptor", "triceratops"]);
19
20console.log(dinoReport); // "REX, RAPTOR, TRICERATOPS"La structure est toujours la même : une parenthèse ouvrante, function(), le corps { /* code */ }, une parenthèse fermante et (), qui appelle la fonction. La parenthèse autour de la fonction la transforme en expression, sans elle le moteur signale une erreur de syntaxe. Quand une IIFE renvoie un objet avec des méthodes, tu obtiens un module avec un état privé :
1const visitorCounter = (function() {
2 let count = 0; // état privé du module
3
4 return {
5 increment() { return ++count; },
6 getValue() { return count; }
7 };
8})();
9
10visitorCounter.increment(); // 1
11visitorCounter.increment(); // 2
12console.log(visitorCounter.getValue()); // 2Les IIFE étaient très populaires avant ES6, quand var n'avait pas de portée de bloc. Aujourd'hui, elles isolent toujours le code, mais dans un nouveau projet je te recommande les modules ES avec import et export. Les IIFE, il vaut mieux savoir les reconnaître dans du code plus ancien.
Le hoisting - var contre let/const
Le hoisting (remontée) est le mécanisme par lequel les déclarations de variables et de fonctions sont "déplacées" au début de leur portée. Chaque mot-clé se comporte cependant différemment :
1// var - remonté avec la valeur undefined
2console.log(dinoVar); // undefined (pas une erreur !)
3var dinoVar = "Stegosaurus";
4console.log(dinoVar); // "Stegosaurus"
5
6// let/const - remontés, mais avec une "Temporal Dead Zone" (TDZ)
7// console.log(dinoLet); // ReferenceError ! La variable est dans la TDZ
8let dinoLet = "Ankylosaurus";
9console.log(dinoLet); // "Ankylosaurus"
10
11// const - comme let, et exige en plus une initialisation immédiate
12// console.log(dinoConst); // ReferenceError ! TDZ
13const dinoConst = "Brachiosaurus";Une variable déclarée avec let existe dès le début du bloc, mais jusqu'à la ligne de déclaration elle se trouve dans une zone interdite, donc la lire se termine par un ReferenceError, et non par la valeur undefined ou null.
var contre let dans les boucles - le piège classique
C'est l'un des problèmes les plus célèbres de JavaScript, où les closures et var donnent des résultats inattendus :
1// Le problème avec var - toutes les closures partagent la même variable
2for (var i = 0; i < 3; i++) {
3 setTimeout(function() {
4 console.log("var: Dinosaure n°" + i); // Toujours "Dinosaure n°3" !
5 }, 100);
6}
7
8// La solution avec let - chaque itération a sa propre copie
9for (let j = 0; j < 3; j++) {
10 setTimeout(function() {
11 console.log("let: Dinosaure n°" + j); // 0, 1, 2 - correct !
12 }, 100);
13}Pourquoi var ne fonctionne-t-il pas ? Il a une portée de fonction, donc les trois closures font référence à une seule variable i, qui vaut 3 après la boucle. let a une portée de bloc, et chaque itération crée une nouvelle variable j. Avant l'arrivée de let, on s'en sortait avec une IIFE qui fige la valeur courante :
1// L'ancienne solution : une IIFE capture la valeur courante
2for (var k = 0; k < 3; k++) {
3 (function(captured) {
4 setTimeout(function() {
5 console.log("IIFE: Dinosaure n°" + captured); // 0, 1, 2
6 }, 100);
7 })(k);
8}Cela fonctionne, mais let fait la même chose sans fonction supplémentaire, c'est donc lui qu'il faut choisir.
Résumé
| Concept | Analogie à Jurassic Park |
|---|---|
| Portée globale | La route principale du parc - visible depuis tous les enclos |
| Portée de fonction | L'enclos d'un dinosaure - il voit dehors, mais personne ne voit dedans |
| Closure | Le dinosaure se souvient du plan du parc même après la reconstruction des clôtures |
| Variables privées | Le code PIN de la clôture - caché dans un système fermé |
| IIFE | Un protocole de sécurité à usage unique - exécute et oublie |
| Hoisting de var | Un dinosaure qui figure sur la liste avant d'être sorti de l'œuf |
| TDZ (let/const) | Une zone interdite jusqu'à l'ouverture officielle de l'enclos |
Les closures reviendront dans le module sur l'asynchronisme, car chaque callback passé à setTimeout ou à une promesse utilise une fermeture.
Pratique
Dans le bac à sable t'attendent la portée lexicale, le traqueur d'observations et le système de sécurité avec son journal privé. Appelle les traqueurs plusieurs fois et vérifie que chacun compte séparément.
Code de cette leçon : index.js
1// Closures et portée lexicale - Parc Jurassique
2console.log("=== CLOSURES DANS LE PARC JURASSIQUE ===\n");
3
4// ================================================
5// 1. PORTÉE LEXICALE - les clôtures du parc
6// ================================================
7console.log("--- 1. Portée lexicale ---");
8
9const parkName = "Jurassic Park"; // Portée globale
10
11function enclosureA() {
12 const dinosaurA = "Tyrannosaurus Rex"; // Portée enclosureA
13
14 function innerFence() {
15 const babyDino = "Baby Rex"; // Portée innerFence
16 // Voit TOUT: ses propres variables + parent + globales
17 console.log("innerFence voit: " + parkName + ", " + dinosaurA + ", " + babyDino);
18 }
19
20 innerFence();
21 // console.log(babyDino); // ERREUR ! Ne voit pas l'intérieur d'innerFence
22 console.log("enclosureA voit: " + parkName + ", " + dinosaurA);
23}
24
25enclosureA();
26
27// ================================================
28// 2. CLOSURE - le dinosaure se souvient de son environnement
29// ================================================
30console.log("\n--- 2. Closure - traqueur de dinosaures ---");
31
32function createDinoTracker(species) {
33 let sightings = 0; // Cette variable vit dans la closure !
34
35 return function reportSighting(location) {
36 sightings++; // La closure se souvient de sightings et le modifie
37 console.log(species + " observé à: " + location + " (observation #" + sightings + ")");
38 };
39}
40
41const trackRex = createDinoTracker("T-Rex");
42const trackRaptor = createDinoTracker("Velociraptor");
43
44trackRex("Secteur A");
45trackRex("Secteur B");
46trackRaptor("Laboratoire");
47trackRex("Secteur C");
48// Chaque traqueur a SA PROPRE copie de la variable sightings !
49
50// ================================================
51// 3. CONFIDENTIALITÉ DES DONNÉES - système de sécurité
52// ================================================
53console.log("\n--- 3. Variables privées (encapsulation) ---");
54
55function createSecuritySystem(enclosureName) {
56 let isLocked = true; // Variable PRIVÉE
57 const log = [];
58
59 return {
60 unlock() {
61 isLocked = false;
62 log.push("Déverrouillé à " + new Date().toLocaleTimeString());
63 console.log(enclosureName + " - clôture DÉSACTIVÉE");
64 },
65 lock() {
66 isLocked = true;
67 log.push("Verrouillé à " + new Date().toLocaleTimeString());
68 console.log(enclosureName + " - clôture ACTIVÉE");
69 },
70 getStatus() {
71 return enclosureName + ": " + (isLocked ? "FERMÉ" : "OUVERT");
72 },
73 getLog() { return [...log]; }
74 };
75}
76
77const rexSecurity = createSecuritySystem("Enclos du T-Rex");
78console.log(rexSecurity.getStatus());
79rexSecurity.unlock();
80console.log(rexSecurity.getStatus());
81rexSecurity.lock();
82console.log("Journal:", rexSecurity.getLog());
83// rexSecurity.isLocked -> undefined (privée !)
84
85// ================================================
86// 4. FABRIQUE DE FONCTIONS
87// ================================================
88console.log("\n--- 4. Fabrique de fonctions ---");
89
90function createDinoFilter(property, value) {
91 return function(dinosaur) {
92 return dinosaur[property] === value;
93 };
94}
95
96const dinosaurs = [
97 { name: "Rex", diet: "carnivore", sector: "A" },
98 { name: "Tricia", diet: "herbivore", sector: "B" },
99 { name: "Blue", diet: "carnivore", sector: "C" },
100 { name: "Steggy", diet: "herbivore", sector: "A" }
101];
102
103const isCarnivore = createDinoFilter("diet", "carnivore");
104const isSectorA = createDinoFilter("sector", "A");
105
106console.log("Carnivores:", dinosaurs.filter(isCarnivore).map(d => d.name));
107console.log("Secteur A:", dinosaurs.filter(isSectorA).map(d => d.name));
108
109// ================================================
110// 5. IIFE - protocole à usage unique
111// ================================================
112console.log("\n--- 5. IIFE ---");
113
114const emergencyReport = (function(dinos) {
115 const carnivores = dinos.filter(d => d.diet === "carnivore");
116 const herbivores = dinos.filter(d => d.diet === "herbivore");
117 return "Rapport: " + carnivores.length + " carnivores, " + herbivores.length + " herbivores";
118})(dinosaurs);
119
120console.log(emergencyReport);
121
122// ================================================
123// 6. HOISTING - var vs let
124// ================================================
125console.log("\n--- 6. Hoisting ---");
126
127// var - hoisting avec undefined
128console.log("var avant la déclaration:", typeof dinoVar); // undefined
129var dinoVar = "Stegosaurus";
130console.log("var après la déclaration:", dinoVar);
131
132// let - zone morte temporelle (TDZ)
133// console.log(dinoLet); // ReferenceError!
134let dinoLet = "Ankylosaurus";
135console.log("let après la déclaration:", dinoLet);
136
137// var vs let dans une boucle
138console.log("\nBoucle avec let:");
139for (let j = 0; j < 3; j++) {
140 setTimeout(() => console.log(" let: Dinosaure #" + j), 10);
141}Retiens bien : une closure, c'est un dinosaure qui se souvient de son enclos, autrement dit une fonction qui emporte avec elle les variables de l'endroit où elle est née.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'allons-nous voir dans la console après l'exécution du code suivant ? function outer() { const x = 10; function inner() { console.log(x); } inner(); } outer();
2. Qu'est-ce qu'une closure en JavaScript ?
3. Que se passe-t-il lorsqu'on essaie d'utiliser une variable déclarée avec let AVANT sa déclaration ? console.log(dinoName); let dinoName = "Rex";
Tâches pratiques dans le jeu
- Éditeur de code
Écris une fonction createDinoTracker(species) qui garde dans une closure un compteur privé sightings et un tableau history, et renvoie un objet avec les méthodes : reportSighting(location) (augmente le compteur et ajoute le lieu à l'historique), getSightings() (renvoie le nombre d'observations) et getHistory() (renvoie une copie de l'historique). Écris aussi createEnclosure(name, maxCapacity) avec un tableau privé residents et les méthodes : addDinosaur(dinoName) (ajoute le dinosaure et renvoie true, ou renvoie false quand il n'y a plus de place), removeDinosaur(dinoName), getPopulation() (renvoie une copie du tableau) et getStatus() (renvoie le texte 'name : X/maxCapacity', par exemple 'Paddock T-Rex: 1/2').
- Tri vertical
Range les étapes de création et d'utilisation d'une closure en JavaScript :
- Clique dans l'ordre
Construis une IIFE (Immediately Invoked Function Expression) correcte en cliquant sur les éléments dans le bon ordre :
- Éditeur de code
Écris une fabrique createFilter(property, value) qui renvoie une fonction vérifiant si objet[property] === value, et une fabrique createWeightConverter(multiplier) qui renvoie une fonction multipliant un poids en kilogrammes par multiplier. Ensuite, crée avec une IIFE un compteur dinoCounter avec une variable privée count et les méthodes increment(), decrement() et getValue().