Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Let const et portee des variables

Cette leçon explique quand utiliser const et quand utiliser let, et pourquoi on n'écrit plus var. Vous verrez que const empêche de remplacer une valeur mais pas de modifier le contenu d'un objet ou d'...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Cette leçon explique quand utiliser const et quand utiliser let, et pourquoi on n'écrit plus var. Vous verrez que const empêche de remplacer une valeur mais pas de modifier le contenu d'un objet ou d'un tableau, et comment Object.freeze() bloque ce contenu au premier niveau. Vous découvrirez les trois niveaux de portée (globale, de fonction et de bloc), le masquage d'une variable, le hoisting et la zone morte temporelle. Enfin, vous apprendrez à éviter les pièges de var : variable qui sort de son bloc, double déclaration, rattachement à window et bug classique des boucles. Des schémas et cinq activités pratiques accompagnent chaque notion.
  1. Objectifs

    • Déclarer des variables avec let et const, et savoir pourquoi on n'utilise plus var
    • Comprendre la notion de portée: globale, de fonction et de bloc
    • Expliquer la remontée (hoisting) et la zone morte temporelle
    • Distinguer réaffecter une variable et modifier le contenu d'un objet déclaré avec const
    • Comprendre le masquage de variables et les fermetures (closures)
    • Appliquer ces règles dans les classes TypeScript d'Angular
  2. Présentation

    • Une variable est un nom qui désigne une valeur stockée en mémoire. En JavaScript, on la crée avec un mot-clé de déclaration: var, let ou const.
    • Jusqu'en 2015, il n'existait que var. Ses règles surprenantes étaient la cause de nombreux bugs. La norme ES6 (ES2015) a introduit let et const, qui sont aujourd'hui la seule façon recommandée de déclarer une variable.
    • Choisir le bon mot-clé, c'est indiquer clairement deux choses à la personne qui lira le code :
      • où la variable existe : sa portée;
      • si sa valeur peut changer : la réaffectation.

Choisir entre let, const et var

  1. Déclarer avec const

    • const déclare une constante: une variable qui reçoit une valeur au moment de sa déclaration et qui ne peut jamais être réaffectée.
    • La valeur est obligatoire à la déclaration.
const tauxTVA = 0.19;
const nomCentre = 'FormaPlus';
const anneeOuverture = 2010;

// tauxTVA = 0.07;
// TypeError: Assignment to constant variable.

// const devise;
// SyntaxError: Missing initializer in const declaration
  • Par convention, les valeurs fixées une fois pour toutes dans le programme (taux, limites, messages) s'écrivent parfois en majuscules: const TVA = 0.19;, const NOMBRE_MAX_STAGIAIRES = 30;.
  • Déclarer avec let

    • let déclare une variable qu'on peut réaffecter. La valeur initiale est facultative : sans valeur, la variable vaut undefined.
    • On l'utilise pour une valeur qui évolue: un compteur, un total, un résultat calculé en plusieurs étapes.
    let total = 0;
    const notes = [12, 15, 9];
    
    for (const note of notes) {
      total = total + note;      // réaffectation : let est nécessaire
    }
    console.log(total);          // 36
    
    let mention;                 // sans valeur : undefined
    if (total / notes.length >= 10) {
      mention = 'Admis';
    } else {
      mention = 'Ajourné';
    }
    console.log(mention);        // Admis
    • Remarque : dans une boucle for…of, on peut écrire const, car une nouvelle variable est créée à chaque tour. Dans une boucle for classique (for (let i = 0; …; i++)), il faut let: i++ réaffecte la variable.
  • const ne rend pas un objet immuable

    • const interdit de remplacer la valeur de la variable. Mais si cette valeur est un objet ou un tableau, son contenu reste modifiable.
    • Pensez à une étiquette collée sur une boîte : const empêche de recoller l'étiquette sur une autre boîte, pas de changer ce qu'il y a dans la boîte.
    const stagiaire = { nom: 'Sami', groupe: 'DEV1' };
    stagiaire.groupe = 'DEV2';        // autorisé : on modifie une propriété
    stagiaire.actif = true;           // autorisé : on ajoute une propriété
    console.log(stagiaire);           // { nom: 'Sami', groupe: 'DEV2', actif: true }
    // stagiaire = { nom: 'Karim' };  // interdit : réaffectation
    
    const notes = [12, 15];
    notes.push(18);                   // autorisé
    console.log(notes);               // [ 12, 15, 18 ]
    // notes = [];                    // interdit
    • Pour empêcher aussi la modification du contenu, on utilise Object.freeze(). Attention : le gel est superficiel, il ne s'applique pas aux objets imbriqués.
    'use strict';
    const parametres = Object.freeze({ taux: 0.19, devise: 'DT' });
    parametres.taux = 0.07;
    // TypeError: Cannot assign to read only property 'taux' of object
    // (sans 'use strict', la modification est ignorée sans message)
    
    const formateur = Object.freeze({ nom: 'Ali', adresse: { ville: 'Tunis' } });
    formateur.adresse.ville = 'Sfax';   // autorisé : adresse n'est pas gelé
    console.log(formateur.adresse.ville);   // Sfax
    1. La portée des variables

      • La portée (scope) est la zone du code où une variable existe et peut être utilisée. Il en existe trois niveaux :
        • la portée globale: une variable déclarée en dehors de toute fonction et de tout bloc est visible dans tout le fichier (dans tout le module) ;
        • la portée de fonction: une variable déclarée dans une fonction n'existe que dans cette fonction ;
        • la portée de bloc: une variable let ou const déclarée dans un bloc { } (if, for, while, switch…) n'existe que dans ce bloc.
      • Règle de visibilité : un bloc voit les variables des blocs qui l'entourent, mais pas celles des blocs qu'il contient.

    Les trois niveaux de portée des variables

    const centre = 'FormaPlus';             // portée globale
    
    function afficherGroupe() {
      const groupe = 'DEV1';                // portée de la fonction
      if (true) {
        const salle = 'B12';                // portée du bloc
        console.log(centre, groupe, salle); // FormaPlus DEV1 B12
      }
      console.log(salle);                   // ReferenceError: salle is not defined
    }
    
    afficherGroupe();
    console.log(groupe);                    // ReferenceError: groupe is not defined
    • Bonne pratique : déclarer chaque variable dans la portée la plus petite possible, au plus près de son utilisation. Moins une variable est visible, moins elle risque d'être modifiée par erreur.
    • Les blocs d'un switch partagent la même portée : pour déclarer une variable du même nom dans plusieurs case, on ajoute des accolades.
    const role = 'admin';
    switch (role) {
      case 'admin': {
        const message = 'Accès total';
        console.log(message);
        break;
      }
      case 'stagiaire': {
        const message = 'Accès limité';   // pas de conflit grâce aux accolades
        console.log(message);
        break;
      }
    }
    1. Le masquage (shadowing)

      • Une variable déclarée dans un bloc peut porter le même nom qu'une variable d'une portée extérieure : elle la masque à l'intérieur du bloc, sans la modifier.
    const message = 'global';
    {
      const message = 'bloc';    // nouvelle variable, qui masque la première
      console.log(message);      // bloc
    }
    console.log(message);        // global : la variable globale n'a pas changé
    • C'est autorisé, mais souvent source de confusion. Préférez des noms différents et explicites (messageGlobal, messageErreur…).
  • La remontée (hoisting) et la zone morte temporelle

    • Avant d'exécuter un bloc, JavaScript parcourt ses déclarations et réserve les variables : c'est la remontée (hoisting). Mais les trois mots-clés ne réagissent pas de la même façon :
      • avec var, la variable existe dès le début et vaut undefined jusqu'à sa déclaration : pas d'erreur, mais une valeur trompeuse ;
      • avec let et const, la variable est réservée mais inaccessible jusqu'à la ligne de sa déclaration. Cette zone s'appelle la zone morte temporelle (Temporal Dead Zone) : toute utilisation provoque une ReferenceError.

    Remontée des variables et zone morte temporelle

    console.log(a);   // undefined
    var a = 3;
    
    console.log(b);   // ReferenceError: Cannot access 'b' before initialization
    let b = 3;
    • Les déclarations de fonction (function nom() { }) sont remontées entièrement : on peut les appeler avant leur définition. Ce n'est pas le cas d'une fonction rangée dans une constante :
    console.log(direBonjour());   // Bonjour : fonction remontée
    function direBonjour() {
      return 'Bonjour';
    }
    
    saluer();                     // ReferenceError: Cannot access 'saluer' before initialization
    const saluer = () => 'Salut';
    1. Les pièges de var

      • var a une portée de fonction: il ignore les blocs { }. Une variable déclarée dans un if ou une boucle « fuit » dans toute la fonction.
    function testerPortee() {
      if (true) {
        var v = 1;
        let l = 2;
      }
      console.log(v);          // 1 : var a fui hors du bloc
      console.log(typeof l);   // undefined : let est resté dans le bloc
    }
    testerPortee();
    • var autorise la redéclaration d'une même variable, ce qui peut écraser une valeur sans le moindre avertissement. let et const l'interdisent :
    var total = 10;
    var total = 20;           // aucune erreur : la première valeur est perdue
    console.log(total);       // 20
    
    let nombre = 1;
    // let nombre = 2;        // SyntaxError: Identifier 'nombre' has already been declared
    • Dans un navigateur, une variable var déclarée au niveau global d'un script classique devient une propriété de l'objet window. Elle peut alors entrer en conflit avec une autre bibliothèque ou un autre script. Ce n'est pas le cas de let et const:
    <script>
      var compteurVar = 1;
      let compteurLet = 2;
      console.log(window.compteurVar);   // 1
      console.log(window.compteurLet);   // undefined
    </script>
    • Le piège le plus célèbre concerne les boucles qui créent des fonctions exécutées plus tard :
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log('var', i), 0);
    }
    for (let j = 0; j < 3; j++) {
      setTimeout(() => console.log('let', j), 0);
    }
    // var 3   var 3   var 3
    // let 0   let 1   let 2

    Une boucle avec var et avec let

    1. Les fermetures (closures)

      • Une fonction se souvient des variables de la portée où elle a été créée, même après la fin de cette portée. C'est ce qu'on appelle une fermeture (closure).
      • Combinée à let, la fermeture permet de créer une variable privée, accessible uniquement par les fonctions qui l'entourent :
    function creerCompteur() {
      let valeur = 0;               // invisible de l'extérieur
      return () => ++valeur;        // la fonction renvoyée se souvient de valeur
    }
    
    const suivant = creerCompteur();
    console.log(suivant(), suivant(), suivant());   // 1 2 3
    console.log(typeof valeur);                     // undefined
    • C'est aussi grâce aux fermetures que la boucle avec let fonctionne : chaque fonction créée dans la boucle se souvient de sa variable j.
  • let et const dans Angular

    • Dans une méthode d'un composant ou d'un service, on déclare les variables locales avec const et let, exactement comme en JavaScript.
    • En revanche, les propriétés d'une classe (les données du composant) se déclarent sans mot-clé. Pour empêcher leur réaffectation, TypeScript fournit le modificateur readonly, l'équivalent de const pour une propriété.
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-panier',
      templateUrl: './panier.html',
    })
    export class Panier {
      readonly tauxTVA = 0.19;          // propriété non réaffectable
      articles: number[] = [45, 20];    // propriété modifiable (sans let ni const)
    
      total() {
        let somme = 0;                  // variable locale réaffectée : let
        for (const prix of this.articles) {
          somme += prix;
        }
        const ttc = somme * (1 + this.tauxTVA);   // variable locale fixe : const
        return ttc.toFixed(3);
      }
    }
    • Les erreurs signalées par le compilateur TypeScript si on se trompe :
    Code écrit dans la classe Message du compilateur
    const tauxTVA = 0.19; (propriété) TS1248: A class member cannot have the 'const' keyword.
    let articles: number[] = []; (propriété) TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
    this.tauxTVA = 0.07; (propriété readonly) TS2540: Cannot assign to 'tauxTVA' because it is a read-only property.
    somme = 10; (variable locale const) TS2588: Cannot assign to 'somme' because it is a constant.
    • À retenir pour Angular : const et let dans les méthodes, rien (ou readonly) devant les propriétés de la classe. Vous retrouvez ainsi dans le code généré par Angular : protected readonly title = signal('mon-projet');.
  • Activité 1 : prédire les portées

    • Dans le dossier rappel-ecmascript de la leçon précédente (dépôt GitHub), créer le fichier 09-portees.js et y recopier le code suivant. Avant de l'exécuter, compléter chaque commentaire // ? par l'affichage attendu, ou par le nom de l'erreur.
    const niveau = 'global';
    
    function tester() {
      const niveau = 'fonction';
      console.log(niveau);                // ?
    
      for (let i = 0; i < 2; i++) {
        const niveau = `boucle ${i}`;
        console.log(niveau);              // ?
      }
    
      try {
        console.log(i);                   // ?
      } catch (e) {
        console.log(e.name);
      }
    
      if (niveau === 'fonction') {
        var drapeau = true;
      }
      console.log(drapeau);               // ?
    }
    
    tester();
    console.log(niveau);                  // ?
    
    try {
      console.log(compteur);              // ?
    } catch (e) {
      console.log(e.name);
    }
    let compteur = 5;
    • Exécuter avec node 09-portees.js, corriger vos prédictions et expliquer chaque erreur de prédiction dans un commentaire.
  • Activité 2 : corriger un code écrit avec var

    • Le programme ci-dessous contient deux bugs liés à var. Le copier dans 10-bugs-var.js et l'exécuter.
    var messages = [];
    for (var i = 1; i <= 3; i++) {
      messages.push(function () {
        return 'Module ' + i;
      });
    }
    messages.forEach(function (m) {
      console.log(m());
    });
    
    function calculerPrime(moyenne) {
      if (moyenne >= 16) {
        var prime = 200;
      }
      return prime;
    }
    console.log(calculerPrime(17), calculerPrime(12));
    • Le résultat obtenu est :
    Module 4
    Module 4
    Module 4
    200 undefined
    • Expliquer en commentaire l'origine de chaque bug.
    • Remplacer tous les var par let ou const, puis exécuter de nouveau : quelle erreur apparaît maintenant dans calculerPrime? Pourquoi est-ce une bonne nouvelle ?
    • Corriger calculerPrime pour qu'elle renvoie 0 quand la moyenne est inférieure à 16. Le résultat attendu est :
    Module 1
    Module 2
    Module 3
    200 0
    1. Activité 3 : un stock protégé par une fermeture

      • Dans 11-stock.js, écrire une fonction creerStock(produit, quantiteInitiale = 0) qui :
        • déclare une variable quantite avec let et un tableau historique avec const;
        • renvoie un objet contenant trois fonctions fléchées : ajouter(n), retirer(n) (qui refuse le retrait si le stock est insuffisant et le note dans l'historique) et etat().
      • Tester avec le code suivant :
    const claviers = creerStock('Clavier', 10);
    claviers.ajouter(5);
    claviers.retirer(12);
    claviers.retirer(8);
    console.log(claviers.etat());
    console.log(claviers.quantite);
    
    const souris = creerStock('Souris');
    souris.ajouter(3);
    console.log(souris.etat());
    
    // Résultat attendu :
    // Clavier : 3 en stock (+5, -12, refus -8)
    // undefined
    // Souris : 3 en stock (+3)
    • Répondre en commentaire : pourquoi claviers.quantite vaut-il undefined? Les deux stocks partagent-ils la même variable quantite? Pourquoi historique peut-il être déclaré avec const alors qu'on y ajoute des éléments ?
  • Activité 4 : const partout où c'est possible

    • Dans 12-refactorisation.js, recopier le code ci-dessous, où toutes les variables sont déclarées avec let. Remplacer par const chaque variable qui n'est jamais réaffectée, puis vérifier que le programme fonctionne toujours.
    let formations = ['Web', 'Réseaux', 'Design'];
    let inscrits = [18, 24, 9];
    let capacite = 20;
    let complet = 0;
    let rapport = '';
    
    for (let i = 0; i < formations.length; i++) {
      let nom = formations[i];
      let nombre = inscrits[i];
      let places = capacite - nombre;
      if (places <= 0) {
        complet++;
        rapport += `${nom} : complet\n`;
      } else {
        rapport += `${nom} : ${places} places\n`;
      }
    }
    
    let resume = `${complet} formation(s) complète(s)`;
    console.log(rapport + resume);
    • Combien de variables restent déclarées avec let? Lesquelles, et pourquoi ?
    • Bonus : réécrire la boucle avec formations.forEach((nom, i) => { … }). Une variable let disparaît-elle ?
  • Activité 5 : repérer les erreurs dans une classe TypeScript

    • Le composant suivant contient quatre erreurs liées à let, const et readonly. Les repérer, puis écrire la version corrigée dans 13-composant.ts en indiquant le message du compilateur pour chacune.
    export class ListeStagiaires {
      const titre = 'Mes stagiaires';
      readonly capacite = 25;
      let stagiaires: string[] = [];
    
      ajouter(nom: string) {
        const nombre = this.stagiaires.length;
        if (nombre >= this.capacite) {
          this.capacite = this.capacite + 5;
        }
        this.stagiaires.push(nom);
        nombre = nombre + 1;
        return nombre;
      }
    }
    • Pour vérifier, compiler le fichier depuis votre dossier avec npx -p typescript tsc --noEmit 13-composant.ts. Le compilateur signale d'abord l'erreur de syntaxe (TS1068: Unexpected token…, causée par let devant une propriété) ; une fois celle-ci corrigée, il affiche les trois autres (TS1248, TS2540, TS2588). Après correction complète, aucune erreur ne doit s'afficher.
    • Deux corrections sont possibles pour capacite: retirer readonly, ou ne plus modifier la capacité. Laquelle choisissez-vous, et pourquoi ?
    • Faire un commit pour chaque activité, puis un push : git commit -m "let, const et portée : activités 1 à 5".
  • Erreurs fréquentes

  • Message Cause Solution
    TypeError: Assignment to constant variable. Réaffectation d'une variable const Utiliser let si la valeur doit vraiment changer
    SyntaxError: Missing initializer in const declaration const déclarée sans valeur Donner la valeur dès la déclaration, ou utiliser let
    SyntaxError: Identifier 'x' has already been declared Deux déclarations du même nom dans la même portée Renommer, ou supprimer la seconde déclaration (réaffecter sans let)
    ReferenceError: Cannot access 'x' before initialization Utilisation dans la zone morte temporelle Déplacer la déclaration avant la première utilisation
    ReferenceError: x is not defined Variable utilisée hors de son bloc, ou faute de frappe Déclarer la variable dans la portée qui l'utilise
    TS1248: A class member cannot have the 'const' keyword. const ou let devant une propriété de classe Supprimer le mot-clé, ou utiliser readonly
    1. À retenir

      • const par défaut ; let seulement si la variable est réaffectée ; plus jamais var.
      • let et const ont une portée de bloc: la variable n'existe qu'entre les accolades où elle est déclarée.
      • Un bloc voit les variables des blocs qui l'entourent, jamais l'inverse.
      • Avant sa ligne de déclaration, une variable let ou const est dans la zone morte temporelle: son utilisation provoque une erreur.
      • const empêche la réaffectation, pas la modification du contenu d'un objet ou d'un tableau.
      • Dans une classe Angular : const / let dans les méthodes, rien ou readonly devant les propriétés.
    Une variable bien déclarée dit au lecteur où elle vit et si elle peut changer : avec const et let, le code explique lui-même ses intentions.