Apprenez en lisant dans l'ordre

Portée et hoisting — où et à partir de quand une variable est lisible

La portée détermine où une variable est lisible, et le hoisting enregistre les déclarations avant l'exécution. Masquage, TDZ et pourquoi éviter var.

Si tu utilises hors des accolades une valeur calculée à l'intérieur, l'exécution s'arrête. Elle s'arrête aussi quand une ligne lit un réglage déclaré plus bas. Dans les deux cas, la question est de savoir où un nom est utilisable, et à partir de quand.

Cet article traite de la portée, qui détermine où un nom déclaré est lisible, et du hoisting, qui enregistre les déclarations avant que le code ne s'exécute.

Des variables valables seulement entre accolades — la portée de bloc

Imagine que tu veuilles afficher sur la page du panier combien il manque au client pour avoir la livraison gratuite. Tu peux calculer ce montant dans le if qui vérifie la livraison gratuite, mais si tu essaies d'afficher cette valeur hors des accolades, l'exécution s'arrête sur une ReferenceError.

La portée (la zone du code dans laquelle un nom est lisible) est délimitée par des accolades. La zone couverte par une paire d'accolades s'appelle la portée de bloc, et une variable déclarée avec const ou let n'est lisible que dans le bloc le plus proche qui entoure sa déclaration.

const cartTotal = 4800;            // Niveau le plus extérieur, hors de toute accolade

function checkShipping(total) {
  const freeLine = 5000;           // Seulement dans les accolades de checkShipping

  if (total < freeLine) {
    const shortage = freeLine - total;  // Seulement dans les accolades du if
    console.log(`Encore ${shortage} yens`);  // Encore 200 yens
  }

  // Les variables déclarées à l'extérieur sont lisibles depuis l'intérieur
  console.log(cartTotal);          // 4800
  // console.log(shortage);        // ReferenceError: shortage is not defined
}

checkShipping(cartTotal);
Les variables lisibles ou non depuis l'extérieur
Niveau le plus extérieur (hors de toute accolade)
  • const cartTotal = 4800 — lisible depuis n'importe où dans le fichier
  • checkShipping(cartTotal) — appelle la fonction depuis le niveau le plus extérieur
Accolades de checkShipping
  • const freeLine = 5000 — lisible seulement dans ces accolades
  • Le cartTotal extérieur reste lisible ici
  • console.log(shortage) — hors du if, shortage est illisible : ReferenceError
Accolades du if
  • const shortage = 200 — lisible seulement dans ces accolades
  • freeLine et cartTotal sont lisibles tous les deux
Le code intérieur peut lire les valeurs de l'extérieur, mais une valeur créée dans le if n'est plus lisible une fois ses accolades refermées. La lecture ne marche que dans un sens : de l'intérieur vers l'extérieur.

Le niveau le plus extérieur, hors de toute accolade, est la portée globale, et les variables qui s'y trouvent sont lisibles de partout. Le corps d'une fonction est lui aussi une paire d'accolades, donc tu ne peux pas lire freeLine depuis l'extérieur de la fonction. La zone délimitée par les accolades d'une fonction s'appelle la portée de fonction.

Affiche aux membres combien de points il leur manque pour atteindre le rang suivant. points et nextRankLine sont déjà déclarés.

① Déclare avec let la variable remaining, qui contiendra les points manquants, à un endroit où elle est lisible aussi en dehors du if.

② Seulement quand les points ne suffisent pas, affecte les points manquants à remaining dans les accolades du if.

③ En dehors du if, affiche le résultat sous la forme « Plus que N points avant le rang suivant ».

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

La déclaration intérieure l'emporte — le masquage (shadowing)

Quand tu ajoutes une fonction qui calcule un prix avec un taux de TVA réduit, il peut t'arriver d'y déclarer une variable taxRate, du même nom que celle de l'extérieur. Si tu ne sais pas quelle valeur est utilisée, tu auras du mal à comprendre pourquoi un résultat est faux.

Si tu déclares dans une portée intérieure un nom qui existe déjà à l'extérieur, c'est la déclaration intérieure qui est utilisée dans ces accolades. On appelle ça le masquage (shadowing : une déclaration intérieure cache une variable extérieure du même nom), et ça se produit aussi bien avec les accolades d'un if qu'avec celles d'une fonction.

const taxRate = 0.1;                 // Taux fixé à l'extérieur

function withTax(price) {
  return Math.floor(price * (1 + taxRate));
}

function withReducedTax(price) {
  const taxRate = 0.08;              // Même nom, déclaré dans la fonction
  return Math.floor(price * (1 + taxRate));
}

console.log(withTax(1000));          // 1100
console.log(withReducedTax(1000));   // 1080
console.log(taxRate);                // 0.1 (celui de l'extérieur n'a pas changé)
Un même nom, une valeur qui dépend de l'endroit où on le lit
const taxRate= 0.1 (extérieur)Lu danswithTaxLu danswithReducedTaxLu horsdes fonctionsPas de déclarationintérieure : 0.1Trouve le 0.08intérieurUtilise le 0.1extérieurRenvoie 1100Renvoie 1080Affiche 0.1
Le même nom taxRate trouve une déclaration différente selon l'endroit où tu le lis. Une déclaration intérieure ne modifie pas la valeur extérieure.

Un nom est d'abord recherché dans les accolades les plus proches de la ligne qui le lit, puis de plus en plus loin vers l'extérieur. Dans withReducedTax, la recherche s'arrête dès qu'elle trouve 0.08, donc le 0.1 extérieur n'est jamais lu. Un code où un même nom désigne une valeur différente dans chaque fonction est difficile à suivre : dans ton propre code, donne-leur des noms distincts, comme reducedTaxRate.

Sans const, tu affectes la variable extérieure

Si tu écris taxRate = 0.08; dans une fonction sans const, aucune nouvelle variable n'est créée : c'est une affectation au taxRate extérieur. Si celui-ci est un const, l'exécution s'arrête sur une TypeError, mais si c'est un let, la valeur est écrasée, et même un appel ultérieur à withTax(1000) renvoie 1080.

Remplace le libellé du prix sur une page produit, mais seulement pendant les soldes. label et isSale sont déjà déclarés.

① Quand isSale vaut true, déclare label avec la valeur « Prix soldé » dans les accolades du if, et affiche-le.

② Affiche label en dehors du if.

③ Écris un second if où le nom intérieur devient saleLabel, et affiche le résultat sous la forme « Prix normal → Prix soldé ».

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Illisible avant la ligne de déclaration — le hoisting et la TDZ

Imagine que tu veuilles regrouper des réglages comme un taux de points ou des frais de port sur des lignes placées sous le code qui les utilise. Comme tu l'as vu dans l'article sur les fonctions, une déclaration function peut être appelée même si elle est écrite plus bas, mais lire un réglage let ou const avant sa ligne de déclaration arrête l'exécution sur une ReferenceError.

Avant d'exécuter une portée, JavaScript enregistre les noms des déclarations qu'elle contient. C'est ce qu'on appelle le hoisting (les déclarations sont enregistrées avant l'exécution du code). Une déclaration function enregistre son nom et son corps ensemble, donc tu peux l'appeler depuis des lignes situées au-dessus de la déclaration. let et const n'enregistrent que le nom : la valeur n'est placée qu'au moment où la ligne de déclaration s'exécute.

// Le corps de la fonction lit pointRate au moment de l'appel
function calcPoint(amount) {
  return Math.floor(amount * pointRate);
}

// Si tu l'appelles ici, la ligne de déclaration de pointRate ne s'est pas encore exécutée
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization

let pointRate = 0.05;              // Réglages regroupés plus bas

console.log(calcPoint(3200));      // 160 (lisible, car la ligne de déclaration s'est exécutée)
À partir de quand une valeur let est lisible
La portée commenceà s'exécuterSeul le nom depointRate est notéJusqu'à laligne letAppel à calcPoint→ ReferenceErrorExécute letpointRate = 0.05Ensuite, l'appelrenvoie 160
Tant que la ligne let ne s'est pas exécutée, la valeur n'existe pas, même quand c'est une fonction écrite plus haut qui la lit. Ce qui compte pour la lecture, c'est l'ordre d'exécution, pas l'endroit où le code est écrit.

L'intervalle entre l'entrée dans une portée et l'exécution de la ligne de déclaration let s'appelle la TDZ (Temporal Dead Zone : la période pendant laquelle un nom est illisible, jusqu'à l'exécution de sa ligne de déclaration). Si le message d'erreur dit is not defined, le nom n'existe pas du tout dans la portée. S'il dit before initialization, le nom existe, mais tu l'as lu avant que sa ligne de déclaration ne s'exécute.

Affiche sur la page de confirmation de commande le montant restant avant la livraison gratuite et les points gagnés. orderTotal est déjà déclaré.

① Déclare avec let le seuil de livraison gratuite freeLine à 5000, et sur la ligne en dessous, affiche « Encore N yens pour la livraison gratuite ».

② Écris d'abord la ligne qui affiche « Points gagnés : N », puis sur la ligne en dessous, déclare le taux de points pointRate avec let à 0.05.

③ Vérifie combien de lignes s'affichent et quelle erreur arrête l'exécution.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Enregistré à l'avance avec undefined — pourquoi ne pas utiliser var

Tu croiseras des variables déclarées avec var dans des articles et des bibliothèques anciens, et si tu recopies du code qui fonctionne, var peut venir avec. Mais var se distingue de const et let sur deux points : la zone où la variable est valable, et ce que tu obtiens en la lisant avant sa déclaration.

Une déclaration var est elle aussi enregistrée à l'avance, mais la variable reçoit undefined au même moment. Du coup, la lire sur une ligne située au-dessus de la déclaration ne provoque pas d'erreur : l'exécution continue simplement avec undefined. Elle n'est pas non plus limitée aux accolades : elle est valable dans toute la fonction qui l'entoure.

function checkStock(orderCount) {
  // La lire avant la déclaration n'arrête rien : elle contient undefined
  console.log(stock);              // undefined

  var stock = 12;
  console.log(stock);              // 12

  if (orderCount <= stock) {
    var status = "Prêt à expédier";          // Déclaré avec var
    let note = "Expédié depuis l'entrepôt A"; // Déclaré avec let
  }
  // var est valable dans toute la fonction, donc lisible en dehors du if
  console.log(status);             // Prêt à expédier
  // console.log(note);            // ReferenceError: note is not defined
}

checkStock(3);
var n'est pas limité par les accolades
var statusaffecté dans le ifSort desaccolades du ifToujours dans lafonction : lisiblePrêt à expédiers'affichelet noteaffecté dans le ifSort desaccolades du ifnote n'existe quedans le ifS'arrête surReferenceError
Même quand l'affectation a lieu dans le même if, une valeur var reste lisible sur les lignes qui suivent la fin des accolades. Avec let, l'exécution s'arrête sur la ligne qui la lit hors de sa zone.

Une fonction n'a qu'une seule variable par nom déclaré avec var, donc redéclarer le même nom avec var dans un if ne masque rien : ça réécrit la même variable. Le tableau ci-dessous montre, pour chaque type de déclaration, ce qui est stocké quand le nom est enregistré avant l'exécution, et ce que tu obtiens en le lisant au-dessus de la ligne de déclaration.

DéclarationEnregistré avant l'exécutionLu avant la déclaration
var stock = 12Le nom et undefinedContinue avec undefined
let stock = 12Le nom seul (pas de valeur)S'arrête sur ReferenceError
const stock = 12Le nom seul (pas de valeur)S'arrête sur ReferenceError

Passer de var à let fait s'arrêter certaines lignes

Quand tu remplaces var par let dans du vieux code, les lignes qui lisaient la variable avant sa déclaration, ou en dehors du if, s'arrêtent sur une ReferenceError. En réalité, ces lignes lisaient depuis le début undefined ou une valeur venue d'autres accolades. Pour les corriger, déplace la déclaration au-dessus de la ligne qui la lit, dans des accolades qui entourent aussi cette ligne.

Observe comment se comporte une variable var selon l'endroit où elle est déclarée. cartTotal est déjà déclaré.

① Écris d'abord la ligne qui affiche « À payer : N yens », en soustrayant discount de cartTotal, puis en dessous déclare discount avec var à 300.

② Sur la ligne suivante, affiche de nouveau le montant à payer sous la même forme.

③ Si le total atteint 5000 yens ou plus, redéclare discount avec var à 800 dans un if.

④ En dehors du if, affiche « Remise : N yens ».

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Que se passe-t-il si tu affiches unit sur la ligne qui suit if (true) { const unit = "yen"; } ?

Question 2En haut du fichier, tu définis une fonction show qui renvoie rate. Tu l'appelles ensuite sur une ligne placée sous let rate = 0.1;. Que renvoie-t-elle ?

Question 3Si tu écris console.log(count); sur une ligne au-dessus de var count = 3;, qu'est-ce qui s'affiche ?