Cours JavaScript et TypeScript · Module 2: Fonctions, tableaux et objets

Le hoisting en JavaScript

22 min de lecture
Dans cette leçon8

Bon retour à Jurassic Park ! Dans la leçon sur la portée des variables, nous avons vérifié où vit une variable, c'est-à-dire quelle est sa portée. Aujourd'hui, nous allons répondre à une question tout aussi importante : à partir de quel moment une variable ou une fonction existe réellement. Le mécanisme qui en décide s'appelle le hoisting, et le connaître te protège de la catégorie de bugs la plus sournoise dans les systèmes du parc - ceux qui ne font rien exploser, mais renvoient en silence une mauvaise valeur. Une variable qui existe mais ne contient rien peut être plus dangereuse qu'une variable qui n'existe pas du tout.

Qu'est-ce que le hoisting ?

Le hoisting (en français "remontée") est le comportement de JavaScript qui consiste à déplacer les déclarations de variables et de fonctions en haut de leur portée - de fonction ou globale - dès la phase de compilation, avant que la première ligne de code ne s'exécute. Le moteur parcourt d'abord tout le code et note les noms qui y apparaissent, et seulement ensuite il commence à l'exécuter. C'est pourquoi nous pouvons faire référence à certaines variables et fonctions avant l'endroit où elles sont écrites dans le fichier.

C'est un peu comme l'inventaire dressé avant que les dinosaures n'arrivent dans leurs enclos : le compilateur JavaScript note d'abord "à quoi s'attendre", et seulement ensuite il exécute les opérations concrètes, ligne par ligne. Seules les déclarations remontent - jamais les valeurs qu'on leur affecte.

Chaque variable suit le même cycle de vie. Le moteur analyse d'abord le code, puis crée le contexte d'exécution et y réserve de la mémoire pour les noms déclarés (c'est précisément le moment du hoisting), et seulement alors il exécute le code : la variable reçoit une valeur et elle est utilisée. Quand plus aucune référence ne mène à la valeur, c'est le ramasse-miettes (garbage collector) qui fait le ménage. Pendant la phase de marquage (mark), il marque tout ce que le programme peut encore atteindre et considère le reste comme bon à supprimer, puis, pendant la phase de nettoyage (sweep), il libère sa mémoire.

Le hoisting des fonctions

Les déclarations de fonction sont remontées en entier : le corps de la fonction voyage avec son nom. Nous pouvons donc appeler une fonction avant qu'elle n'apparaisse dans le fichier. Dans la gestion du parc, c'est très pratique, car cela permet de garder la logique principale en haut et de repousser les détails techniques vers le bas.

1// On peut appeler la fonction avant de l'avoir définie
2checkSecurity("Enclos du T-Rex");  // "Vérification de la sécurité : Enclos du T-Rex"
3
4// La déclaration de fonction est remontée
5function checkSecurity(location) {
6  console.log(`Vérification de la sécurité : ${location}`);
7  return true;
8}

Cela fonctionne parce que, pendant la phase de compilation, JavaScript remonte en haut de la portée la déclaration entière - le nom avec le corps. Avant que la première instruction ne s'exécute, checkSecurity est déjà une fonction complète, prête à être appelée, et non un emplacement vide qui attend d'être rempli. C'est le seul cas dans tout le langage où une valeur remonte en haut de la portée en même temps que le nom, car une fonction est elle aussi une valeur. Toutes les autres constructions que nous allons voir dans un instant ne remontent que le nom, et rien d'autre.

Attention aux expressions de fonction !

Seules les déclarations de fonction sont remontées en entier. Les expressions de fonction, c'est-à-dire les fonctions affectées à une variable, suivent les règles du hoisting des variables : la variable elle-même remonte, mais pas la fonction qui lui est affectée.

1// Ceci NE fonctionnera PAS correctement
2// feedDinosaur("Rex");  // TypeError: feedDinosaur is not a function
3
4// Seule la déclaration de la variable est remontée, pas l'affectation de la fonction
5var feedDinosaur = function(dinoName) {
6  console.log(`Nourrissage du dinosaure : ${dinoName}`);
7};
8
9// À partir de cette ligne, on peut appeler la fonction
10feedDinosaur("Rex");  // "Nourrissage du dinosaure : Rex"

Lis attentivement le message d'erreur, car c'est un indice précieux. Nous n'obtenons pas un ReferenceError, mais un TypeError: feedDinosaur is not a function - le nom feedDinosaur existe déjà, il contient simplement undefined à ce moment-là, et on ne peut pas appeler undefined comme une fonction. Cette petite différence peut réduire le débogage d'une heure à une minute : un ReferenceError nous oriente vers une faute de frappe ou un import oublié, alors qu'un TypeError nous oriente vers l'ordre des lignes dans le fichier. Une déclaration de fonction est disponible dans toute sa portée, une expression de fonction seulement à partir de la ligne de l'affectation.

Les fonctions fléchées fonctionnent de la même façon

Les fonctions fléchées sont elles aussi des expressions, leur corps n'est donc jamais remonté. Comme on les affecte généralement à const ou à let, l'erreur en cas d'appel trop précoce se révèle encore plus ferme qu'avec var.

1// checkFence();  // ReferenceError: Cannot access checkFence before initialization
2
3const checkFence = () => "Clôture OK";
4
5console.log(checkFence());  // "Clôture OK"

La différence est importante : avec var, nous avons obtenu un TypeError, ici un ReferenceError. Les deux messages veulent dire la même chose - "trop tôt" - mais la version avec const arrête le programme exactement à l'endroit de l'erreur, au lieu de laisser passer une valeur undefined qui explosera quelques dizaines de lignes plus loin, dans un morceau de code parfaitement innocent. D'où une règle pratique : avant la ligne de définition, on ne peut utiliser que les fonctions déclarées avec function nom() {}. Tout ce qu'on affecte à une variable - expression de fonction classique ou fonction fléchée - ne fonctionne qu'en dessous.

Le hoisting des variables

Pour les variables, le hoisting se comporte différemment selon le mot-clé (var, let, const). C'est de là que viennent la plupart des surprises, alors décortiquons les deux cas.

Le hoisting des variables déclarées avec var

Les variables déclarées avec var sont remontées et immédiatement initialisées à undefined, donc les lire avant la déclaration est parfaitement légal :

1console.log(dinosaurCount);  // undefined (pas une erreur !)
2var dinosaurCount = 15;
3console.log(dinosaurCount);  // 15

Rien n'a cassé - et c'est justement là que se trouve le problème. Le programme continue avec une valeur que personne n'a prévue, et la valeur undefined du compteur de dinosaures se transforme en NaN dans chaque calcul où elle se retrouve. Le rapport affichera NaN au lieu du nombre d'animaux dans un enclos qui en abrite trois, et personne ne recevra le moindre avertissement. C'est plus facile à comprendre si on imagine qu'avant l'exécution, JavaScript réécrit le code ci-dessus sous cette forme :

1var dinosaurCount;           // Hoisting - la déclaration remonte en haut
2console.log(dinosaurCount);  // undefined
3dinosaurCount = 15;          // L'affectation reste là où nous l'avons écrite
4console.log(dinosaurCount);  // 15

La déclaration a sauté en haut, l'affectation est restée exactement là où nous l'avons écrite. Toute la règle du hoisting tient en une phrase : la déclaration remonte, la valeur ne bouge pas. L'intervalle entre ces deux points est une fenêtre pendant laquelle la variable est accessible, mais vide. Dans les logs, un capteur du parc qui renvoie undefined ressemble de façon inquiétante à un capteur qui ne renvoie rien du tout - or ce sont deux situations complètement différentes, l'une signifie le calme, l'autre une clôture arrachée.

Le hoisting des variables déclarées avec let et const

Les variables déclarées avec let et const (introduites par ES6) sont elles aussi remontées, mais elles ne reçoivent pas la valeur undefined. À la place, elles se retrouvent dans la temporal dead zone, c'est-à-dire la zone morte temporelle - la période qui va du début de l'exécution du bloc jusqu'à la ligne où la variable est effectivement déclarée. Y accéder pendant ce temps se termine par une erreur.

1// Ceci provoquera une erreur :
2// console.log(securityLevel);  // ReferenceError: Cannot access securityLevel before initialization
3let securityLevel = "Élevé";
4console.log(securityLevel);  // "Élevé"
5
6// Il en va de même pour const :
7// console.log(parkName);  // ReferenceError: Cannot access parkName before initialization
8const parkName = "Jurassic Park";
9console.log(parkName);  // "Jurassic Park"

Il faut savoir distinguer l'un de l'autre deux messages d'erreur :

  • une variable qui n'a jamais été déclarée : ReferenceError: xyz is not defined
  • une variable let ou const dans la zone morte temporelle : ReferenceError: Cannot access 'xyz' before initialization

Cette distinction est un cadeau pour qui débogue. Le premier cas signifie que la variable n'existe nulle part - très probablement une faute de frappe ou un import oublié. Le second dit que la variable existe, mais que nous l'avons lue trop tôt, il suffit donc de déplacer la lecture sous la déclaration. Imagine une fonction de deux cents lignes dans laquelle la valeur est lue en haut et affectée en bas : avec var, nous obtenons un undefined silencieux et une heure de recherche à tâtons, avec let, le programme s'arrête immédiatement et montre du doigt la ligne précise. La zone morte temporelle n'est pas une limitation, c'est un système d'alerte précoce.

Les classes aussi restent dans la zone morte temporelle

Les classes se comportent comme let et const, et non comme les déclarations de fonction. Leur nom est remonté, mais tant que le moteur n'a pas exécuté le corps de la classe, toute tentative de l'utiliser se termine par une erreur.

1// const dino = new Dinosaur("Rex");  // ReferenceError: Cannot access Dinosaur before initialization
2
3class Dinosaur {
4  constructor(name) {
5    this.name = name;
6  }
7}
8
9const dino = new Dinosaur("Rex");  // OK
10console.log(dino.name);  // "Rex"

La règle "on peut définir les fonctions sous l'endroit où on les utilise" ne s'étend donc pas aux classes - une classe doit exister avant le premier new qui crée son instance. Si, par habitude, tu repousses la définition d'une classe à la fin du fichier, c'est précisément ce message que tu verras dans la console, et le remède est banal : déplace la classe au-dessus du code qui l'instancie. Dans les fichiers du parc, cela donne un ordre simple - d'abord les modèles des animaux et des équipements, ensuite la logique qui les utilise, et jamais l'inverse.

Le hoisting et la portée des variables

Le hoisting est étroitement lié à la portée des variables, qu'on appelle aussi leur scope. Les déclarations ne remontent pas en haut du fichier, mais en haut de leur portée, et la portée qui est "la leur" dépend du mot-clé : var s'en tient à la portée de fonction, let et const - à la portée de bloc. Dans la portée globale, la règle reste la même, sauf que le haut est tout simplement le début du programme.

Exemple pour var (portée de fonction)

var ignore complètement les accolades d'un if ou d'une boucle : la déclaration atterrit en haut de la fonction qui la contient, quel que soit l'endroit où nous l'avons écrite.

1function monitorSystem() {
2  console.log(systemStatus);  // undefined (hoisting à l'intérieur de la fonction)
3
4  if (true) {
5    var systemStatus = "Online";
6  }
7
8  console.log(systemStatus);  // "Online"
9}
10
11monitorSystem();
12// console.log(systemStatus);  // ReferenceError: systemStatus is not defined (hors de la portée de la fonction)

Ici, le bloc if n'a pas du tout créé de nouvelle portée pour systemStatus. La déclaration a sauté en haut de monitorSystem, donc on peut lire la variable avant même le bloc (elle est alors vide), et après le bloc, elle contient déjà la valeur affectée. En dehors de la fonction, elle disparaît complètement, car la seule frontière que var respecte, c'est la fonction. Les deux console.log avec le même nom donnent donc deux résultats différents - non pas parce qu'il y a deux variables, mais parce que nous regardons une seule variable, une fois avant l'affectation et une fois après.

Les boucles montrent cette différence le plus clairement

Le meilleur terrain d'essai pour comparer les deux portées, ce sont les boucles, car le compteur y est déclaré et utilisé au même endroit. Vérifie ce qui reste après une boucle dont le compteur est créé avec var, et ce qui reste après une boucle avec let.

1for (var index = 0; index < 3; index++) {
2  // ronde des enclos
3}
4console.log(index);  // 3 - var survit à la boucle (portée de fonction)
5
6for (let step = 0; step < 3; step++) {
7  // ronde des enclos
8}
9// console.log(step);  // ReferenceError - let reste dans la portée de bloc

Après la fin de la première boucle, index vaut toujours 3, car il appartient à la portée de fonction et survit tranquillement à l'accolade fermante. La variable step de la seconde boucle a une portée de bloc et disparaît avec la boucle, donc la lire plus bas se termine par une erreur ReferenceError. Cette différence est à l'origine des fameux soucis de compteur dans les boucles avec délai : avec var, tous les appels voient la même valeur finale, avec let, chaque itération reçoit sa propre copie du compteur et garde la sienne.

Exemple pour let et const (portée de bloc)

Avec let et const, chaque paire d'accolades crée une nouvelle portée, et la zone morte temporelle concerne précisément ce bloc, et non la fonction entière.

1function checkEnclosures() {
2  // console.log(enclosureStatus);  // ReferenceError (zone morte temporelle)
3
4  if (true) {
5    let enclosureStatus = "Secure";
6    console.log(enclosureStatus);  // "Secure"
7  }
8
9  // console.log(enclosureStatus);  // ReferenceError (hors de la portée du bloc)
10}
11
12checkEnclosures();

Les deux lignes commentées se termineraient par une erreur, mais pour des raisons complètement différentes : la première parce que la variable se trouve encore dans la zone morte temporelle, la seconde parce que le bloc if s'est terminé et que la variable a cessé d'exister. Les accolades sont ici un véritable mur, et c'est exactement ce que nous voulons - une variable de bloc reste dans son enclos. Dans l'exercice qui t'attend juste après cette leçon, tu t'entraîneras sur la portée des variables dans différents contextes : tu créeras des variables avec différentes portées - globale, de fonction et de bloc - et tu observeras leur comportement.

Exemples pratiques de hoisting à Jurassic Park

Exemple 1 : le système de surveillance des dinosaures

Le hoisting des fonctions permet d'organiser un fichier par ordre d'importance, et non par ordre d'exécution. La procédure principale va en haut, les fonctions utilitaires qu'elle utilise tout en bas - et tout fonctionne toujours.

1// Système de surveillance des dinosaures
2
3// Les fonctions principales sont définies en haut pour plus de clarté
4function initMonitoringSystem() {
5  console.log("Initialisation du système de surveillance des dinosaures...");
6
7  // On peut appeler ces fonctions, même si elles sont définies plus loin
8  const healthStatus = checkDinosaurHealth("Rex");
9  const locationData = trackDinosaur("Blue");
10
11  return {
12    health: healthStatus,
13    location: locationData,
14    systemStatus: "Online"
15  };
16}
17
18// Appel de la fonction principale
19const systemStatus = initMonitoringSystem();
20console.log(systemStatus);
21
22// Fonctions utilitaires définies en bas du fichier
23function checkDinosaurHealth(dinoName) {
24  console.log(`Vérification de l'état de santé du dinosaure : ${dinoName}`);
25  return "En bonne santé";
26}
27
28function trackDinosaur(dinoName) {
29  console.log(`Suivi du dinosaure : ${dinoName}`);
30  return { x: 135, y: 270, area: "Secteur B" };
31}

L'appel à initMonitoringSystem tombe sur une ligne qu'aucune des fonctions utilitaires n'a encore "atteinte", et pourtant le code s'exécute sans accroc. C'est du pur hoisting de déclarations de fonction : les deux étaient complètes avant que la première instruction ne démarre. Pendant l'exécution du programme, plus rien ne se déplace par magie, et le gain est purement humain - celui qui ouvre ce fichier lit d'abord ce que fait le système, et seulement ensuite de quelle manière.

Exemple 2 : le piège du hoisting

Le même mécanisme peut se retourner contre nous. Le hoisting devient traître quand une fonction intérieure déclare une variable dont le nom existe déjà dans la portée extérieure. Lis attentivement le code et essaie de prédire le résultat avant de regarder l'explication.

1// Un piège lié au hoisting
2
3// Fonction qui vérifie les dispositifs de sécurité
4function checkSecurity() {
5  var fenceStatus = "Offline"; // Remontée en haut de la fonction
6
7  function enableFences() {
8    // La variable locale est remontée en haut de enableFences,
9    // mais jusqu'à la ligne d'initialisation, elle n'a aucune valeur
10    console.log("Statut actuel de la clôture avant le changement :", fenceStatus); // undefined !
11
12    // La variable locale fenceStatus masque celle de la portée supérieure
13    var fenceStatus = "Online";
14    console.log("Nouveau statut de la clôture :", fenceStatus); // "Online"
15  }
16
17  enableFences();
18  console.log("Statut de la clôture dans la fonction principale :", fenceStatus); // "Offline" (non modifié)
19}
20
21checkSecurity();

Un piège subtil se cache ici : le console.log à l'intérieur de enableFences affiche undefined, car la variable locale fenceStatus a été remontée en haut de la fonction intérieure et, dès la première ligne, elle masque celle de la portée extérieure, alors qu'elle ne reçoit sa valeur que plus bas. Quant à la variable extérieure, elle ne change jamais non plus - jusqu'à la fin, elle vaut "Offline". Dans le vrai parc, cela donne un panneau d'opérateur qui annonce des clôtures activées, alors que la fonction principale de sécurité les considère toujours comme désactivées.

Exemple 3 : une meilleure approche avec let et const

Remplacer var par let et const élimine toute cette catégorie de problèmes, car une lecture trop précoce arrête le programme au lieu de produire undefined en silence.

1// Une meilleure approche avec let et const
2
3function parkOperations() {
4  // On utilise const pour les valeurs qui ne doivent pas changer
5  const maxVisitors = 2000;
6
7  // On utilise let pour les variables qui vont changer
8  let currentVisitors = 0;
9
10  function admitVisitors(count) {
11    // Avec let et const, lire une variable avant sa déclaration provoque une erreur,
12    // ce qui permet de repérer le problème bien plus facilement
13
14    // Ceci provoquerait une erreur :
15    // console.log(availableSpace);  // ReferenceError: Cannot access before initialization
16
17    // D'abord on déclare, ensuite on utilise
18    const availableSpace = maxVisitors - currentVisitors;
19    if (count <= availableSpace) {
20      currentVisitors += count;
21      console.log(`${count} visiteurs admis. Actuellement dans le parc : ${currentVisitors}`);
22      return true;
23    }
24
25    console.log(`Trop de visiteurs ! Places disponibles : ${availableSpace}`);
26    return false;
27  }
28
29  // On teste notre fonction
30  admitVisitors(500);  // 500 visiteurs admis
31  admitVisitors(1000); // 1000 visiteurs admis
32  admitVisitors(700);  // Trop de visiteurs ! Places disponibles : 500
33
34  return currentVisitors;
35}
36
37console.log(`Nombre total de visiteurs : ${parkOperations()}`);

Voyons ce qui n'a pas changé ici : maxVisitors et currentVisitors sont toujours remontés, car toute déclaration est soumise au hoisting. Ce qui a changé, c'est la réaction à l'erreur - la zone morte temporelle transforme une lecture trop précoce en erreur bruyante au lieu d'un undefined discret. Chaque valeur naît exactement là où elle est nécessaire, et pas un instant plus tôt, si bien que la logique des portiques d'entrée se lit de haut en bas, sans avoir à garder les règles du hoisting en tête. C'est exactement ce qui compte en pratique : non pas apprendre le mécanisme par cœur, mais écrire du code dans lequel ce mécanisme ne surprend jamais.

Conseils pratiques sur le hoisting

  1. Déclare toujours tes variables en haut de leur portée - cela réduit au minimum les problèmes liés au hoisting et rend le code beaucoup plus facile à lire.

  2. Préfère let et const à var - les erreurs de zone morte temporelle sont incomparablement plus faciles à diagnostiquer que de mystérieuses valeurs undefined qui se révèlent plusieurs centaines de lignes plus loin.

  3. Déclare tes fonctions avant de les utiliser - le hoisting des fonctions fonctionne, mais l'ordre naturel de lecture reste de toute façon le plus lisible pour la personne qui reviendra sur ce code dans six mois.

  4. Évite les déclarations de fonction à l'intérieur des blocs conditionnels - le comportement peut varier d'un environnement à l'autre, il est donc plus sûr d'utiliser dans ce cas une expression de fonction affectée à une variable.

  5. Souviens-toi que le hoisting concerne les déclarations, et non les initialisations - l'affectation d'une valeur se produit exactement là où nous l'avons écrite, jamais avant.

Un exemple plus grand : le système de gestion des incidents du parc

Ci-dessous, tu trouveras un système plus complet de gestion des incidents de Jurassic Park, qui rassemble en un seul endroit tout ce que contient cette leçon : des déclarations de fonction remontées, une variable var au niveau du module, ainsi que let et const utilisés pour l'état qui change et pour l'état qui ne doit pas changer.

1// Système de gestion des incidents de Jurassic Park
2
3// Utilisation du hoisting - on déclare les variables au début
4var activeIncidents = [];
5let alertLevel = "Normal";
6const MAX_INCIDENTS = 5;
7
8// Fonction principale d'initialisation du système
9function initIncidentSystem() {
10  // Cette variable est remontée uniquement à l'intérieur de cette fonction
11  var systemStatus = "Initialisation";
12
13  // On profite du hoisting des fonctions - on peut les appeler avant leur définition
14  logSystemStatus();
15  resetIncidents();
16
17  systemStatus = "Online";
18  logSystemStatus();
19
20  // Fonction locale - visible uniquement à l'intérieur de initIncidentSystem
21  function logSystemStatus() {
22    console.log(`Statut du système : ${systemStatus}`);
23  }
24
25  return {
26    reportIncident,      // Référence à une fonction définie plus bas
27    checkAlertLevel,     // Fonction définie plus bas
28    getActiveIncidents,  // Fonction définie plus bas
29    resetIncidents       // Fonction définie à l'intérieur de initIncidentSystem
30  };
31
32  // Fonction locale que l'on renvoie comme partie de l'API
33  function resetIncidents() {
34    console.log("Réinitialisation de la liste des incidents...");
35    activeIncidents = [];
36    updateAlertLevel();
37  }
38}
39
40// Les déclarations de fonction sont remontées, on peut donc les appeler avant leur définition
41// Fonction pour signaler un nouvel incident
42function reportIncident(location, type, severity) {
43  console.log(`Signalement d'incident : ${type} à l'emplacement ${location} (niveau de menace : ${severity})`);
44
45  const incident = {
46    id: generateIncidentId(),
47    location,
48    type,
49    severity,
50    timestamp: new Date().toISOString(),
51    status: "Actif"
52  };
53
54  activeIncidents.push(incident);
55
56  if (activeIncidents.length > MAX_INCIDENTS) {
57    declareEmergency();
58  } else {
59    updateAlertLevel();
60  }
61
62  return incident;
63}
64
65// Génération d'un ID d'incident unique
66function generateIncidentId() {
67  return `INC-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
68}
69
70// Mise à jour du niveau d'alerte en fonction des incidents actifs
71function updateAlertLevel() {
72  const highSeverityCount = activeIncidents.filter(inc => inc.severity === "Élevé").length;
73
74  if (highSeverityCount >= 3) {
75    alertLevel = "Critique";
76  } else if (highSeverityCount > 0 || activeIncidents.length >= 3) {
77    alertLevel = "Renforcé";
78  } else if (activeIncidents.length > 0) {
79    alertLevel = "Vigilance";
80  } else {
81    alertLevel = "Normal";
82  }
83
84  console.log(`Niveau d'alerte mis à jour : ${alertLevel}`);
85}
86
87// Fonction appelée en cas de trop nombreux incidents
88function declareEmergency() {
89  alertLevel = "Évacuation";
90  console.log("ATTENTION ! Trop d'incidents actifs. Nous déclenchons l'évacuation du parc !");
91  // Code pour lancer les protocoles d'évacuation...
92}
93
94// Fonction pour vérifier le niveau d'alerte actuel
95function checkAlertLevel() {
96  return {
97    level: alertLevel,
98    incidentCount: activeIncidents.length,
99    timestamp: new Date().toISOString()
100  };
101}
102
103// Fonction qui renvoie la liste des incidents actifs
104function getActiveIncidents() {
105  return [...activeIncidents]; // On renvoie une copie pour que personne ne modifie l'original
106}
107
108// Initialisation du système
109const incidentSystem = initIncidentSystem();
110
111// Test du système
112console.log("===== TEST DU SYSTÈME DE GESTION DES INCIDENTS =====");
113
114// Vérification de l'état initial
115console.log("Niveau d'alerte initial :", incidentSystem.checkAlertLevel().level);
116
117// Signalement d'incidents
118incidentSystem.reportIncident("Enclos du T-Rex", "Clôture endommagée", "Élevé");
119incidentSystem.reportIncident("Centre des visiteurs", "Panne de courant", "Moyen");
120
121// Vérification de l'état après les signalements
122console.log("Incidents actifs :", incidentSystem.getActiveIncidents().length);
123console.log("Niveau d'alerte actuel :", incidentSystem.checkAlertLevel().level);
124
125// Ajout d'autres incidents de haute priorité
126incidentSystem.reportIncident("Laboratoire", "Fuite de matériel génétique", "Élevé");
127incidentSystem.reportIncident("Enclos des vélociraptors", "Tentative d'évasion", "Élevé");
128
129// Devrait déclencher l'état d'évacuation
130incidentSystem.reportIncident("Secteur B", "Dinosaure hors de son enclos", "Élevé");
131incidentSystem.reportIncident("Centre des opérations", "Panne des systèmes de sécurité", "Élevé");
132
133// Réinitialisation du système
134incidentSystem.resetIncidents();
135console.log("Après la réinitialisation - niveau d'alerte :", incidentSystem.checkAlertLevel().level);

Suis l'ordre des événements dans ce fichier. initIncidentSystem appelle logSystemStatus et resetIncidents avant que l'une ou l'autre n'apparaisse dans le code, et resetIncidents se trouve même après l'instruction return - pourtant tout fonctionne, car les déclarations de fonction sont complètes avant l'exécution de la première ligne du corps. Avec les variables, c'est différent : var systemStatus peut être lue dès le tout début de la fonction, mais elle ne reçoit la valeur "Initialisation" que sur sa propre ligne, et activeIncidents, alertLevel et MAX_INCIDENTS ont été placées volontairement tout en haut du fichier, pour que personne ne les lise trop tôt.

Résumé

Le hoisting est un concept important de JavaScript, qui façonne la manière dont notre code s'exécute :

  1. Les déclarations de fonction sont remontées en entier - on peut les utiliser avant leur déclaration.

  2. Les variables var sont remontées et initialisées à undefined - on peut y faire référence plus tôt, mais jusqu'à l'affectation, elles ne portent aucune valeur utile.

  3. Les variables let et const (et avec elles les classes) sont remontées, mais restent dans la zone morte temporelle - une tentative de lecture avant la déclaration se termine par une erreur.

  4. Les expressions de fonction, y compris les fonctions fléchées, suivent les règles des variables - la variable est remontée, la fonction affectée, non.

Comprendre le hoisting est essentiel pour écrire du code JavaScript prévisible. C'est exactement comme à Jurassic Park - connaître les règles et les protocoles suffit pour que la journée se passe sans surprises ni situations dangereuses.

"Le hoisting en JavaScript ressemble à la mise en route matinale du parc", dit le Dr Rex. "Avant l'arrivée des visiteurs, avant que le code ne démarre, une partie des préparatifs se fait toute seule : les protocoles sont prêts, les procédures en place. Mais les vraies données, il faut encore les recueillir au fil de l'eau, et aller les chercher trop tôt ne te dira absolument rien !"

Dans l'une des prochaines leçons, nous nous attaquerons au mot-clé this - un autre élément fondamental de JavaScript, qui sait aussi semer une belle pagaille.

Code de cette leçon : index.js
1// Hoisting en JavaScript - Parc Jurassique
2console.log("Laboratoire du Hoisting");
3console.log("Comprendre le hoisting en JavaScript\n");
4
5// ===========================================
6// 1. QU'EST-CE QUE LE HOISTING ?
7// ===========================================
8console.log("=== 1. Introduction au hoisting ===");
9
10// JavaScript "remonte" les déclarations en haut de la portée
11// On peut utiliser une fonction avant sa déclaration !
12
13roarDinosaur("T-Rex"); // Fonctionne !
14
15function roarDinosaur(name) {
16  console.log(`${name} rugit: ROAAARRR!`);
17}
18
19roarDinosaur("Velociraptor");
20
21console.log("\nLa fonction est utilisable avant sa déclaration grâce au hoisting !");
22
23// ===========================================
24// 2. HOISTING DES FONCTIONS
25// ===========================================
26console.log("\n=== 2. Hoisting des fonctions ===");
27
28// Les déclarations de fonctions sont remontées
29feedDinosaur("Triceratops"); // Fonctionne
30
31function feedDinosaur(dino) {
32  console.log(`Alimentation: ${dino}`);
33}
34
35// Les expressions de fonction ne sont PAS remontées
36// feedDino2("Stegosaurus"); // ERREUR !
37
38const feedDino2 = function(dino) {
39  console.log(`Alimentation: ${dino}`);
40};
41
42feedDino2("Stegosaurus"); // Maintenant, ça fonctionne
43
44// Les fonctions fléchées ne sont PAS remontées non plus
45// feedDino3("Brachiosaurus"); // ERREUR !
46
47const feedDino3 = (dino) => {
48  console.log(`Alimentation: ${dino}`);
49};
50
51feedDino3("Brachiosaurus"); // Maintenant, ça fonctionne
52
53// ===========================================
54// 3. HOISTING DE VAR
55// ===========================================
56console.log("\n=== 3. Hoisting des variables var ===");
57
58console.log("\nAvant la déclaration:");
59console.log("dinoName:", typeof dinoName); // undefined (pas une erreur !)
60
61var dinoName = "T-Rex";
62
63console.log("Après la déclaration:");
64console.log("dinoName:", dinoName); // T-Rex
65
66// En réalité, JavaScript l'interprète ainsi:
67// var dinoName; // Déclaration remontée en haut
68// console.log(dinoName); // undefined
69// dinoName = "T-Rex"; // Affectation
70
71// ===========================================
72// 4. HOISTING DE LET ET CONST
73// ===========================================
74console.log("\n=== 4. Hoisting let et const ===");
75
76// let et const sont aussi remontés, mais...
77// se trouvent dans la "zone morte temporelle" (TDZ)
78
79console.log("\nTentative d'utilisation avant la déclaration:");
80
81// console.log(dinoSpecies); // ReferenceError: Cannot access before initialization
82// console.log(parkName); // ReferenceError: Cannot access before initialization
83
84let dinoSpecies = "Velociraptor";
85const parkName = "Jurassic Park";
86
87console.log("Après la déclaration:");
88console.log("dinoSpecies:", dinoSpecies);
89console.log("parkName:", parkName);
90
91// ===========================================
92// 5. ZONE MORTE TEMPORELLE (TDZ)
93// ===========================================
94console.log("\n=== 5. Zone morte temporelle (TDZ) ===");
95
96function demonstrateTDZ() {
97  console.log("\nDans la fonction - avant la déclaration:");
98
99  // Cette zone est la TDZ pour 'dinosaur'
100  // console.log(dinosaur); // ERREUR dans la TDZ
101
102  let dinosaur = "Triceratops";
103
104  console.log("Après la déclaration:", dinosaur); // Fonctionne
105}
106
107demonstrateTDZ();
108
109// ===========================================
110// 6. DIFFÉRENCES: var vs let vs const
111// ===========================================
112console.log("\n=== 6. Comparaison: var vs let vs const ===");
113
114function compareHoisting() {
115  console.log("\nTest du hoisting:");
116
117  // var - remonté avec la valeur undefined
118  console.log("var dinosaur1:", typeof dinosaur1); // undefined
119  var dinosaur1 = "T-Rex";
120  console.log("var dinosaur1:", dinosaur1); // T-Rex
121
122  // let - remonté, mais dans la TDZ
123  // console.log("let dinosaur2:", dinosaur2); // ERREUR: TDZ
124  let dinosaur2 = "Velociraptor";
125  console.log("let dinosaur2:", dinosaur2); // Velociraptor
126
127  // const - remonté, mais dans la TDZ
128  // console.log("const dinosaur3:", dinosaur3); // ERREUR: TDZ
129  const dinosaur3 = "Triceratops";
130  console.log("const dinosaur3:", dinosaur3); // Triceratops
131}
132
133compareHoisting();
134
135// ===========================================
136// 7. HOISTING DANS LES BLOCS
137// ===========================================
138console.log("\n=== 7. Hoisting dans les blocs ===");
139
140function blockHoisting() {
141  console.log("\nAvant le bloc:");
142  console.log("globalVar:", typeof globalVar); // undefined
143  // console.log("blockLet:", typeof blockLet); // ERREUR
144
145  var globalVar = "Disponible partout";
146
147  if (true) {
148    console.log("\nDans le bloc:");
149    let blockLet = "Seulement dans le bloc";
150    const blockConst = "Seulement dans le bloc (const)";
151
152    console.log("blockLet:", blockLet);
153    console.log("blockConst:", blockConst);
154    console.log("globalVar:", globalVar);
155  }
156
157  console.log("\nAprès le bloc:");
158  console.log("globalVar:", globalVar); // Fonctionne
159  // console.log("blockLet:", blockLet); // ERREUR
160  // console.log("blockConst:", blockConst); // ERREUR
161}
162
163blockHoisting();
164
165// ===========================================
166// 8. HOISTING DANS LES BOUCLES
167// ===========================================
168console.log("\n=== 8. Hoisting dans les boucles ===");
169
170console.log("\nBoucle avec var:");
171for (var i = 0; i < 3; i++) {
172  console.log(`  Itération ${i + 1}`);
173}
174console.log("Après la boucle - i:", i); // Fonctionne (i = 3)
175
176console.log("\nBoucle avec let:");
177for (let j = 0; j < 3; j++) {
178  console.log(`  Itération ${j + 1}`);
179}
180// console.log("Après la boucle - j:", j); // ERREUR: j is not defined
181
182// ===========================================
183// 9. PROBLÈMES PRATIQUES AVEC LE HOISTING
184// ===========================================
185console.log("\n=== 9. Pièges courants ===");
186
187// Problème 1: Écrasement de fonction
188function problemExample1() {
189  console.log("\nProblème 1: Écrasement");
190
191  function greet() {
192    console.log("  Premier salut");
193  }
194
195  greet(); // Quelle fonction sera appelée ?
196
197  function greet() {
198    console.log("  Deuxième salut (c'est celui-ci qui sera utilisé !)");
199  }
200
201  // JavaScript "remonte" les deux déclarations,
202  // mais la seconde écrase la première
203}
204
205problemExample1();
206
207// Problème 2: var dans une boucle avec setTimeout
208console.log("\nProblème 2: var dans une boucle avec setTimeout");
209
210console.log("Avec var (toutes afficheront 3):");
211for (var k = 0; k < 3; k++) {
212  setTimeout(function() {
213    console.log(`  var k = ${k}`);
214  }, 100);
215}
216
217setTimeout(() => {
218  console.log("\nAvec let (afficheront 0, 1, 2):");
219  for (let m = 0; m < 3; m++) {
220    setTimeout(function() {
221      console.log(`  let m = ${m}`);
222    }, 100);
223  }
224}, 200);
225
226// ===========================================
227// 10. BONNES PRATIQUES
228// ===========================================
229console.log("\n=== 10. Bonnes pratiques ===");
230
231// BONNES PRATIQUES:
232
233// 1. Déclare les variables au début de la portée
234function goodPractice1() {
235  const parkName = "Jurassic Park";
236  let dinosaurCount = 0;
237  let totalVisitors = 0;
238
239  dinosaurCount = 15;
240  totalVisitors = 1000;
241
242  console.log(`\n${parkName}: ${dinosaurCount} dinosaures, ${totalVisitors} visiteurs`);
243}
244
245goodPractice1();
246
247// 2. Utilise const par défaut, let si nécessaire
248const SECURITY_LEVEL = "ÉLEVÉ";
249let currentStatus = "OPÉRATIONNEL";
250
251console.log(`Sécurité: ${SECURITY_LEVEL}`);
252console.log(`Statut: ${currentStatus}`);
253
254// 3. Évite var
255// var oldWay = "Ne l'utilise pas !"; // MAUVAIS
256
257// 4. Déclare les fonctions avant de les utiliser (lisibilité)
258function wellOrganized() {
259  // Déclarations au début
260  const MAX_CAPACITY = 50;
261  let currentCapacity = 0;
262
263  // Fonctions auxiliaires
264  function addDinosaur(name) {
265    if (currentCapacity < MAX_CAPACITY) {
266      currentCapacity++;
267      console.log(`  Ajout de ${name}. Capacité: ${currentCapacity}/${MAX_CAPACITY}`);
268    } else {
269      console.log(`  Pas de place pour ${name}`);
270    }
271  }
272
273  // Logique principale
274  console.log("\nSystème de gestion de la capacité:");
275  addDinosaur("T-Rex");
276  addDinosaur("Velociraptor");
277  addDinosaur("Triceratops");
278}
279
280wellOrganized();
281
282// ===========================================
283// 11. EXEMPLE COMPLET
284// ===========================================
285console.log("\n=== 11. Système de surveillance - exemple complet ===");
286
287function createMonitoringSystem() {
288  // Déclarations au début (bonne pratique)
289  const systemName = "Dino Monitor v1.0";
290  let isActive = false;
291  let alertCount = 0;
292
293  // Fonctions auxiliaires
294  function activate() {
295    isActive = true;
296    console.log(`\n${systemName} activé`);
297  }
298
299  function deactivate() {
300    isActive = false;
301    console.log(`${systemName} désactivé`);
302  }
303
304  function sendAlert(message) {
305    if (isActive) {
306      alertCount++;
307      console.log(`Alerte #${alertCount}: ${message}`);
308    } else {
309      console.log("Système inactif - alerte ignorée");
310    }
311  }
312
313  function getStats() {
314    console.log(`\nStatistiques ${systemName}:`);
315    console.log(`   Statut: ${isActive ? "Actif" : "Inactif"}`);
316    console.log(`   Alertes envoyées: ${alertCount}`);
317  }
318
319  // Interface principale
320  return {
321    start: activate,
322    stop: deactivate,
323    alert: sendAlert,
324    stats: getStats
325  };
326}
327
328const monitor = createMonitoringSystem();
329
330monitor.alert("Test avant activation"); // Ignorée
331monitor.start();
332monitor.alert("Le T-Rex s'approche de la clôture");
333monitor.alert("Niveau de sécurité critique");
334monitor.stats();
335monitor.stop();
336monitor.alert("Test après désactivation"); // Ignorée
337monitor.stats();
338
339console.log("\nFélicitations ! Tu maîtrises maintenant le hoisting en JavaScript !");

Tu as repéré une erreur dans cette leçon ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Déclare une constante globale parkName = 'Jurassic Park'. Écris une fonction checkEnclosure() qui crée une constante locale enclosureName = 'Zone A' et, dans un bloc if (true), crée avec let une variable alarmLevel = 5 puis affiche parkName, enclosureName et alarmLevel dans la console. La fonction renvoie enclosureName. Appelle checkEnclosure(). En dehors de la fonction, enclosureName et alarmLevel ne doivent pas exister.

Articles utiles