Les Variables
en JavaScript
De var à const — portée, hoisting, TDZ, types, coercition. Les fondations que tout le monde utilise mais que peu comprennent vraiment.
Tu vas construire une carte de profil dynamique qui affiche les données d'un utilisateur en temps réel — nom, âge, ville — tout en visualisant les types JS de chaque valeur. C'est le projet fil rouge de ce cours.
const / let)typeofIntroduction aux variables
Une variable est une boîte étiquetée qui stocke une valeur en mémoire. En JavaScript, trois mots-clés permettent d'en créer : var, let et const — chacun avec des règles bien distinctes.
const est un casier verrouillé avec un cadenas — tu peux y mettre des affaires, mais tu ne peux pas remplacer le casier lui-même. let est un casier ordinaire — tu peux vider et remplir à volonté. var est un casier sans porte dans un couloir partagé — tout le monde y a accès et tu sais jamais qui a touché à tes affaires.
var
Héritage ES1 (1995). Portée fonction, hoisting bizarre, re-déclaration silencieuse. À éviter dans tout code moderne.
let
ES6. Portée bloc, TDZ, pas de re-déclaration. Le choix par défaut pour les valeurs qui changent.
const
ES6. Portée bloc, liaison immuable (pas la valeur !). À utiliser par défaut — pas de réassignation.
// Les 3 façons de déclarer une variable const nom = "Alice"; // ← par défaut : const let score = 0; // ← si la valeur va changer var age = 30; // ← à éviter // const sans valeur → erreur immédiate const x; // SyntaxError: Missing initializer // let sans valeur → undefined let y; // y === undefined
var, let, const — les différences
var — pourquoi c'est problématique
var ignore les blocs {} et s'échappe dans la portée parente. Elle autorise aussi la re-déclaration silencieuse — un bug qui peut passer des heures sans être détecté.
// ① Portée fonction (pas bloc) if (true) { var x = 10; // var ignore le bloc {} } console.log(x); // 10 ← visible ici ! // ② Re-déclaration silencieuse var utilisateur = "Alice"; // ... 200 lignes plus tard ... var utilisateur = "Bob"; // aucune erreur ! Alice écrasée silencieusement // ③ Bug classique des boucles for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Affiche : 3, 3, 3 (pas 0, 1, 2 !) // var i est partagée entre toutes les itérations
const — la liaison immuable
const crée une liaison immuable, pas une valeur immuable. On ne peut pas réassigner la variable, mais on peut muter l'objet ou tableau qu'elle référence.
const PI = 3.14; PI = 3; // TypeError: Assignment to constant variable ✓ // ⚠️ const n'empêche PAS la mutation const user = { nom: "Alice" }; user.nom = "Bob"; // ✓ OK — on mute l'objet, pas la liaison user = {}; // TypeError — réassignation interdite // Pour vraiment geler un objet : const config = Object.freeze({ api: "https://..." }); config.api = "autre"; // silencieusement ignoré
On déclare les données du profil en choisissant consciemment const ou let selon si la valeur peut changer.
// Données fixes → const const NOM = "Alice"; const DATE_NAISSANCE = "1996-04-12"; // Données variables → let let score = 0; let estConnecte = false; let ville = "Paris"; // Objet profil complet → const (mutation autorisée) const profil = { nom : NOM, age : 28, ville : ville, }; console.log(profil); // { nom: "Alice", age: 28, ville: "Paris" }
const. Passe à let uniquement quand ton éditeur te signale que tu réassignes. Cette règle simple élimine 80% des bugs liés aux variables.
Que se passe-t-il si on essaie d'ajouter une propriété à un objet déclaré avec const ?
Types de données
JavaScript est un langage à typage dynamique — une même variable peut contenir des types différents au cours de l'exécution. Il existe 7 types primitifs et 1 type objet.
const txt = "Bonjour"; // string const n = 42; // number (entiers ET flottants) const big = 9007199254740993n; // bigint const ok = true; // boolean const vide = null; // null (absence intentionnelle) let u; // undefined (non initialisé) const id = Symbol("id"); // symbol (unique) // Type object (tout le reste) const obj = { a: 1 }; // object const arr = [1, 2]; // object (tableau) const fn = () => {}; // function
typeof — inspecter le type
typeof "hello" // "string" typeof 42 // "number" typeof true // "boolean" typeof undefined // "undefined" typeof {} // "object" typeof [] // "object" ← tableau = objet typeof null // "object" ← bug historique de JS ! typeof function(){} // "function" // Vérification fiable de null : value === null // Vérification fiable de tableau : Array.isArray([]); // true ✓
On enrichit la carte de profil pour afficher le type JS de chaque valeur — comme un inspecteur de données en temps réel.
function afficherProfil(profil) { const champs = Object.entries(profil); return champs.map(([cle, valeur]) => ` <div class="champ"> <span class="cle">${cle}</span> <span class="valeur">${valeur}</span> <span class="type">typeof: ${typeof valeur}</span> </div> `).join(''); } const profil = { nom : "Alice", // string age : 28, // number actif : true, // boolean telephone : null, // object ← bug typeof ! }; document.getElementById('carte').innerHTML = afficherProfil(profil);
Que retourne typeof null en JavaScript ?
Portée (Scope)
La portée définit où une variable est accessible. JavaScript a 4 niveaux : globale, module, fonction et bloc.
// 1. Portée globale const GLOBAL = "visible partout"; function externe() { // 2. Portée fonction const locale = "dans externe()"; function interne() { // 3. Portée imbriquée — closure console.log(GLOBAL); // ✓ accessible console.log(locale); // ✓ accessible (closure) // 4. Portée bloc if (true) { let bloc = "seulement ici"; } // console.log(bloc); // ReferenceError } } // console.log(locale); // ReferenceError — invisible dehors
Hoisting & Temporal Dead Zone
Le hoisting est le mécanisme par lequel JS déplace conceptuellement les déclarations en haut de leur portée avant l'exécution. Le comportement est très différent selon var, let/const et les fonctions.
// var : hissée ET initialisée à undefined console.log(x); // undefined (pas d'erreur !) var x = 5; // Fonctions : entièrement hissées direBonjour(); // ✓ fonctionne avant la déclaration function direBonjour() { console.log("Bonjour !"); } // let / const : hissées mais PAS initialisées → TDZ console.log(nom); // ReferenceError: Cannot access before initialization let nom = "Alice"; // ← fin de la TDZ
| Mot-clé | Hissé ? | Initialisé ? | Avant déclaration ? |
|---|---|---|---|
| var | ✅ Oui | ✅ undefined | ⚠️ Oui (undefined) |
| let | ✅ Oui | ❌ Non | 🚫 Non (TDZ) |
| const | ✅ Oui | ❌ Non | 🚫 Non (TDZ) |
| function | ✅ Oui | ✅ Complète | ✅ Oui |
Coercition de types
JavaScript convertit automatiquement les types lors de certaines opérations — c'est la coercition implicite. Source de nombreux comportements surprenants.
// + avec string = concaténation "3" + 4 // "34" 1 + 2 + "3" // "33" (1+2=3, puis 3+"3") // Autres opérateurs = coercition numérique "5" - 2 // 3 true + 1 // 2 (true → 1) null + 1 // 1 (null → 0) // == coerce, === ne coerce pas "1" == 1 // true ← dangereux "1" === 1 // false ← correct [] == false // true 😱 [] == ![] // true 😱😱
Conversion explicite — la bonne pratique
// → Number Number("42") // 42 Number("abc") // NaN +"42" // 42 (unary plus) parseInt("42px") // 42 // → String String(42) // "42" (42).toString(2) // "101010" (binaire) // → Boolean Boolean(0) // false Boolean("0") // true ! (string non vide) Boolean([]) // true ! (objet) !!value // double négation — idiomatique
On ajoute une fonction de validation qui s'assure que l'âge est bien un nombre et que le nom n'est pas vide — avec des conversions explicites.
function validerProfil({ nom, age, ville }) { const erreurs = []; // Nom : doit être une string non vide const nomStr = String(nom).trim(); if (!nomStr) erreurs.push("Le nom est requis"); // Âge : convertir et vérifier que c'est un entier valide const ageNum = Number(age); if (Number.isNaN(ageNum) || ageNum < 0 || ageNum > 150) { erreurs.push("L'âge doit être un nombre entre 0 et 150"); } if (erreurs.length) throw new Error(erreurs.join(", ")); return { nom : nomStr, age : Math.floor(ageNum), ville: String(ville).trim() || "Non renseigné", }; }
Nullish & Valeurs Falsy
Certaines valeurs sont considérées comme fausses dans un contexte booléen. Connaître la différence entre falsy et nullish évite de nombreux bugs subtils.
8 valeurs Falsy
false · 0 · -0 · 0n · "" · null · undefined · NaN
Tout le reste est Truthy
"0" · "false" · [] · {} · -1 · Infinity · toute fonction
// || valeur par défaut — attention aux falsy ! const nom = "" || "Anonyme"; // "Anonyme" (car "" est falsy) const port = 0 || 3000; // 3000 — bug ! 0 est falsy // ?? nullish coalescing — uniquement null/undefined const nom2 = "" ?? "Anonyme"; // "" ← correct ! const port2 = 0 ?? 3000; // 0 ← correct ! // ??= assigne seulement si null/undefined let config = {}; config.debug ??= false; // assigne seulement si absent config.nom ||= "défaut"; // assigne si falsy
On assemble la version finale du profil en appliquant toutes les conventions de nommage et en utilisant ?? pour les valeurs optionnelles.
// UPPER_SNAKE_CASE pour les constantes de config const AGE_MAX = 150; const NOM_PAR_DEFAUT = "Anonyme"; // camelCase pour les variables const estConnecte = true; // booléen → préfixe "est"/"is" const hasAvatar = false; // booléen → préfixe "has" // Profil final avec valeurs par défaut via ?? function creerProfil(data = {}) { return { nom : (String(data.nom ?? "").trim() || NOM_PAR_DEFAUT), age : Math.min(Math.abs(Number(data.age) || 0), AGE_MAX), ville : String(data.ville ?? "Non renseigné").trim(), creeLe : new Date().toLocaleDateString(), }; } const alice = creerProfil({ nom: "Alice", age: 28, ville: "Paris" }); console.log(alice); // { nom: "Alice", age: 28, ville: "Paris", creeLe: "..." }
Pièges classiques
Avec var, toutes les itérations d'une boucle partagent la même variable i. Au moment où le callback s'exécute, la boucle est terminée et i vaut sa valeur finale.
// ❌ avec var — bug for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 3, 3, 3 ← var i est partagée // ✅ avec let — chaque itération a son propre i for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 0, 1, 2 ✓
JS utilise la norme IEEE 754 pour les flottants. Certaines valeurs décimales ne peuvent pas être représentées exactement en binaire, ce qui crée des imprécisions.
0.1 + 0.2 === 0.3 // false ! (0.30000000000000004) // ✅ Comparer avec une tolérance (epsilon) Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true ✓ // ✅ Pour de l'argent, utiliser des entiers const prixCentimes = 1099; // 10,99€ en centimes
NaN est la seule valeur JS qui n'est pas égale à elle-même. Ne jamais tester x === NaN — utiliser Number.isNaN().
const x = Number("abc"); // NaN x === NaN // false ! toujours x !== x // true ← NaN est l'unique exception // ✅ La bonne façon Number.isNaN(x) // true ✓ isNaN("abc") // true mais coerce d'abord — éviter Number.isNaN("abc") // false ← plus fiable
Exercices
Écris une fonction inspecter(valeurs) qui prend un tableau et retourne un tableau d'objets { valeur, type } en utilisant typeof. Pour null, renvoie "null" (pas "object"). Pour les tableaux, renvoie "array".
function inspecter(valeurs) { return valeurs.map(v => { let type; if (v === null) type = "null"; else if (Array.isArray(v)) type = "array"; else type = typeof v; return { valeur: v, type }; }); } inspecter(["Alice", 42, true, null, undefined, [], {}]); // [ // { valeur: "Alice", type: "string" }, // { valeur: 42, type: "number" }, // { valeur: true, type: "boolean" }, // { valeur: null, type: "null" }, ← pas "object" // { valeur: undefined, type: "undefined" }, // { valeur: [], type: "array" }, ← pas "object" // { valeur: {}, type: "object" }, // ]
Crée un module de conversion de températures avec trois fonctions : celsiusVers(valeur, unite) qui convertit vers "F" ou "K", et normaliser(valeur) qui accepte une string ou un nombre et lève une erreur si la valeur est NaN après conversion.
function normaliser(valeur) { const n = Number(valeur); if (Number.isNaN(n)) { throw new Error(`"${valeur}" n'est pas une température valide`); } return n; } function celsiusVers(valeur, unite) { const c = normaliser(valeur); const conversions = { F: c => c * 9/5 + 32, K: c => c + 273.15, }; const fn = conversions[unite.toUpperCase()]; if (!fn) throw new Error(`Unité inconnue : ${unite}`); return Math.round(fn(c) * 100) / 100; } celsiusVers(100, "F"); // 212 celsiusVers("0", "K"); // 273.15 (string acceptée) celsiusVers("abc", "F"); // Error: "abc" n'est pas une température valide
Finalise la carte de profil du cours. Crée une fonction genererProfil(donnees) qui : (1) valide que nom est une string non vide, (2) convertit age en nombre entier, (3) utilise ?? pour les champs optionnels (ville, bio), (4) retourne un objet avec initiales calculées depuis le nom.
const VILLE_DEFAUT = "Non renseignée"; const BIO_DEFAUT = "Aucune bio"; function genererInitiales(nom) { return nom .split(" ") .filter(Boolean) .map(mot => mot[0].toUpperCase()) .join(""); } function genererProfil(donnees = {}) { // 1. Valider le nom const nomStr = String(donnees.nom ?? "").trim(); if (!nomStr) throw new Error("Le nom est obligatoire"); // 2. Convertir l'âge const ageNum = Math.floor(Number(donnees.age)); if (Number.isNaN(ageNum) || ageNum < 0) { throw new Error("L'âge doit être un nombre positif"); } // 3. Champs optionnels avec ?? return { nom : nomStr, initiales : genererInitiales(nomStr), // "Alice Martin" → "AM" age : ageNum, ville : String(donnees.ville ?? VILLE_DEFAUT).trim(), bio : String(donnees.bio ?? BIO_DEFAUT).trim(), creeLe : new Date().toLocaleDateString("fr-FR"), }; } const alice = genererProfil({ nom: "Alice Martin", age: "28", ville: "Paris" }); // { nom: "Alice Martin", initiales: "AM", age: 28, ville: "Paris", ... }
Référence rapide
| Critère | var | let | const |
|---|---|---|---|
| Portée | Fonction | Bloc | Bloc |
| Hoisting | Oui (undefined) | Oui (TDZ) | Oui (TDZ) |
| Re-déclaration | ✅ Oui | 🚫 Non | 🚫 Non |
| Réassignation | ✅ Oui | ✅ Oui | 🚫 Non |
| Init. obligatoire | Non | Non | Oui |
| Usage | 🚫 Jamais | Valeurs mutables | Par défaut |
const par défaut. 2. Passe à let uniquement si tu réassignes. 3. N'utilise jamais var. Ces 3 règles éliminent 90% des bugs liés aux variables.