Module 2 : JavaScript

Comprendre le fonctionnement de JS

Plongez au cœur du moteur JavaScript, comprenez la pile d'exécution, le tas mémoire et le cycle d'événements.

45 min
Version Audio
Débutant
🏗️

COMPRENDRE LE FONCTIONNEMENT DE JAVASCRIPT

Version Ultra-Détaillée - Définitions & Explications Complètes

🏗️ 1. COMMENT LE NAVIGATEUR EXÉCUTE JAVASCRIPT

1.1 Le Processus Complet

Définition : L'exécution de JavaScript par le navigateur est un processus en plusieurs étapes qui transforme le code source en instructions exécutables par le processeur. Chaque étape a un rôle spécifique et utilise des composants dédiés du moteur JavaScript.

Termes clés à retenir :

  • Moteur JavaScript : Le logiciel qui exécute le code JS (ex: V8 pour Chrome, SpiderMonkey pour Firefox, JavaScriptCore pour Safari).
  • Parser : Le composant qui analyse la syntaxe du code source.
  • Interpréteur : Exécute le bytecode ligne par ligne (Ignition pour V8).
  • Compilateur JIT : Compile les fonctions "chaudes" en code machine pour les accélérer (TurboFan pour V8).
  • Garbage Collector : Nettoie automatiquement la mémoire non utilisée (Orinoco pour V8).
  • Event Loop : Gère les opérations asynchrones et les événements.

┌─────────────────────────────────────────────────────────────────┐

│ PROCESSUS D'EXÉCUTION JS │

│ │

│ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │

│ │ TÉLÉCHARGEMENT │ → │ PARSING │ → │ COMPILATION JIT │ │

│ │ (HTML/CSS/JS) │ │ (AST) │ │ (Bytecode) │ │

│ └─────────────┘ └─────────────┘ └───────────────────┘ │

│ │

│ ┌─────────────────────────────────────────────────────────┐ │

│ │ EXÉCUTION │ │

│ │ ┌──────────────┐ ┌──────────────┐ │ │

│ │ │ CALL STACK │ │ HEAP MEMORY │ │ │

│ │ │ (Exécution) │ │ (Stockage) │ │ │

│ │ └──────────────┘ └──────────────┘ │ │

│ │ │ │

│ │ ┌──────────────────────────────────────────────────┐ │ │

│ │ │ EVENT LOOP │ │ │

│ │ │ Callback Queue → Call Stack (quand vide) │ │ │

│ │ └──────────────────────────────────────────────────┘ │ │

│ └─────────────────────────────────────────────────────────┘ │

│ │

│ ┌─────────────────────────────────────────────────────────┐ │

│ │ GARBAGE COLLECTOR │ │

│ │ (Nettoyage mémoire) │ │

│ └─────────────────────────────────────────────────────────┘ │

└─────────────────────────────────────────────────────────────────┘

Étape Description Composant
1. Téléchargement Le navigateur télécharge le fichier JS depuis le serveur via HTTP. Réseau / HTML Parser
2. Parsing Analyse syntaxique du code source pour vérifier sa validité. Parser (Analyseur syntaxique)
3. AST Conversion du code en Arbre Syntaxique Abstrait (représentation structurée). Parser
4. Bytecode Génération d'instructions intermédiaires (bytecode) exécutables par l'interpréteur. Ignition (V8) / Interpréteur
5. Compilation JIT Compilation des fonctions fréquemment appelées en code machine natif. TurboFan (V8) / Compilateur JIT
6. Exécution Exécution du code sur la Call Stack (pile d'exécution). Moteur JS
7. Event Loop Gestion des opérations asynchrones (timers, I/O, événements). libuv (Node.js) / Moteur JS
8. Garbage Collector Nettoyage automatique de la mémoire inutilisée. Orinoco (V8) / GC

Détail méconnu : Le moteur V8 (Google Chrome) utilise un interpréteur (Ignition) pour générer du bytecode, puis un compilateur JIT (TurboFan) pour optimiser les fonctions "chaudes" en code machine natif. Cette approche hybride offre un bon équilibre entre performance et temps de démarrage.

1.2 Le Parsing (Analyse Syntaxique)

Définition : Le parsing est la première étape d'exécution d'un programme JavaScript. Le moteur lit le code source caractère par caractère et le transforme en une structure compréhensible. C'est comme un professeur de grammaire qui analyse une phrase : il identifie chaque mot, sa nature et sa fonction dans la phrase.

Termes clés :

  • Tokens : Les plus petites unités lexicales du code (mots-clés, identifiants, opérateurs, etc.).
  • AST (Abstract Syntax Tree) : Une représentation arborescente du code qui reflète sa structure syntaxique.
  • Scanning : La phase où le code source est transformé en une liste de tokens.
  • Syntactic Analysis : La phase où les tokens sont transformés en AST selon les règles grammaticales du langage.

Le Scanneur (Lexical Analysis)

Rôle : Le scanneur parcourt le code source caractère par caractère et identifie les tokens. Chaque token a un type (mot-clé, identifiant, opérateur, etc.) et une valeur.

Exemple concret : Pour le code let message = "Bonjour";, le scanneur produit les tokens suivants :

  • KEYWORD : 'let'
  • IDENTIFIER : 'message'
  • OPERATOR : '='
  • STRING : '"Bonjour"'
  • PUNCTUATOR : ';'

Le Parseur (Syntactic Analysis)

Rôle : Le parseur prend la liste des tokens et construit un AST (Abstract Syntax Tree). L'AST est une arborescence qui représente la structure grammaticale du code.

Métaphore : Si le code est une phrase, l'AST en est le diagramme grammatical qui montre comment les mots sont reliés entre eux. Par exemple, la phrase "Le chat mange la souris" a un sujet (le chat), un verbe (mange) et un complément (la souris).

Pourquoi l'AST est important ? L'AST est la structure centrale utilisée par le moteur pour :

  • Vérifier la syntaxe (détection des erreurs).
  • Générer du bytecode pour l'exécution.
  • Optimiser le code via des analyses statiques.

1.3 Compilation & Interprétation

Définition : JavaScript est un langage interprété mais avec des mécanismes de compilation Just-In-Time (JIT). Cela signifie que le code n'est pas compilé avant l'exécution (comme en C++), mais qu'il est traduit en instructions machines pendant l'exécution.

Les deux approches :

  • Interprétation : Le code source (ou le bytecode) est exécuté ligne par ligne par un interpréteur. C'est plus lent mais permet un démarrage rapide.
    Exemple : Les premiers moteurs JS (SpiderMonkey dans Firefox 1.0) étaient purement interprétés.
  • Compilation JIT : Le code est compilé en code machine pendant l'exécution. Une fois compilé, le code machine est exécuté directement par le processeur, ce qui est beaucoup plus rapide.
    Exemple : V8 (Chrome), TurboFan (compilateur optimisant).

Le Bytecode

Définition : Le bytecode est un jeu d'instructions intermédiaire entre le code source et le code machine. Il est plus compact que le code source et peut être exécuté plus rapidement par l'interpréteur.

Métaphore : Le bytecode est comme les notes de musique d'une partition : elles ne sont pas la musique elle-même (le code machine), mais elles contiennent toutes les informations nécessaires pour la jouer (l'exécuter).

Avantages du bytecode :

  • Portabilité : Le même bytecode peut être exécuté sur différentes architectures (CPU).
  • Compacité : Le bytecode est plus petit que le code source, ce qui réduit la mémoire utilisée.
  • Vitesse : L'interpréteur peut exécuter du bytecode plus rapidement que du code source car il n'a pas besoin de le re-analyser.

La Compilation JIT (Just-In-Time)

Définition : La compilation JIT (Just-In-Time) est une technique où le code est compilé en code machine pendant l'exécution du programme, juste avant d'être exécuté. Le mot "Just-In-Time" signifie "juste à temps".

Métaphore : Imaginez un traducteur qui traduit un discours en temps réel. Si le discours est lent, il peut traduire phrase par phrase (interprétation). Si le discours s'accélère, il peut commencer à traduire des blocs entiers à l'avance (JIT). Si le discours est très répétitif, il peut même mémoriser certaines traductions pour les réutiliser (optimisation JIT).

Les étapes du JIT :

  • Profilage : Le moteur surveille l'exécution et identifie les fonctions appelées fréquemment (fonctions "chaudes").
  • Compilation Baseline : Les fonctions "chaudes" sont compilées en code machine (compilateur Baseline). Ce code est déjà plus rapide que l'interprétation.
  • Optimisation : Si une fonction continue d'être appelée, elle est re-compilée avec des optimisations plus poussées (compilateur optimisant comme TurboFan). Le code est inliné, les types sont spécialisés, etc.
  • Dé-optimisation : Si les hypothèses d'optimisation sont invalidées (ex: un paramètre change de type), le moteur revient à une version moins optimisée.

1.4 La Call Stack (Pile d'Appels)

Définition : La Call Stack est une structure de données qui enregistre l'état des appels de fonction en cours. C'est une pile (LIFO - Last In, First Out) : la dernière fonction appelée est la première à être terminée.

Métaphore : Imaginez une pile d'assiettes. Vous posez une assiette (appel de fonction), puis une autre par-dessus, puis une autre. Pour retirer une assiette, vous devez d'abord retirer celle du dessus (la dernière ajoutée). C'est exactement comme la Call Stack.

Rôle de la Call Stack :

  • Garder trace de l'ordre des appels de fonction.
  • Stocker le contexte de chaque fonction (variables locales, paramètres).
  • Retourner à la fonction appelante quand une fonction se termine.
  • Gérer les erreurs (stack trace).

Exemple d'exécution : Prenons ce code :

function

first() { console.log('first'); second(); }

function

second() { console.log('second'); third(); }

function

third() { console.log('third'); }

first();

Déroulement :

  • 1. first() est appelée → elle est empilée sur la Call Stack.
  • 2. first() appelle second() → second() est empilée par-dessus.
  • 3. second() appelle third() → third() est empilée par-dessus.
  • 4. third() se termine → elle est dépilée.
  • 5. second() se termine → elle est dépilée.
  • 6. first() se termine → elle est dépilée.

Détail méconnu : La Call Stack a une taille limitée (environ 10 000 frames dans V8). Si vous dépassez cette taille (ex: récursion infinie), vous obtenez une erreur "Maximum call stack size exceeded".

1.5 La Heap Memory (Tasse Mémoire)

Définition : La Heap (ou tas) est une zone de mémoire où sont stockés les objets, les tableaux, les fonctions et toutes les données de taille variable. Contrairement à la Call Stack, la Heap n'est pas organisée de manière stricte (LIFO) et est gérée par le Garbage Collector.

Métaphore : Si la Call Stack est une pile d'assiettes organisée, la Heap est un grand entrepôt où les objets sont posés n'importe où. Vous avez une étiquette (référence) pour chaque objet, mais vous devez les chercher dans l'entrepôt quand vous en avez besoin.

Que contient la Heap ?

  • Les objets créés avec new Object() ou {}.
  • Les tableaux (Array).
  • Les fonctions (qui sont des objets en JavaScript).
  • Les données volumineuses (plus de 1 MB).
  • Les closures qui capturent des variables.

1.6 Le Garbage Collector (GC)

Définition : Le Garbage Collector (GC) est un mécanisme automatique qui libère la mémoire occupée par les objets qui ne sont plus utilisés. Sans GC, la mémoire finirait par se remplir et le programme planterait (fuite de mémoire).

Métaphore : Le GC est comme un agent de nettoyage dans un grand magasin. Il passe régulièrement pour jeter les objets qui ne sont plus utilisés par personne. Si un objet n'est plus dans le caddie (référence) de personne, il est jeté.

Les algorithmes de GC :

  • Mark-and-Sweep (Marquage puis Balayage) : Le GC part des racines (variables globales, Call Stack) et "marque" tous les objets accessibles. Ensuite, il "balaye" la mémoire et libère les objets non marqués.
  • Generational GC : La mémoire est divisée en générations (jeune et vieille). Les objets jeunes sont collectés plus souvent (Minor GC, rapide). Les objets qui survivent sont promus en vieille génération (Major GC, plus lent).
  • Incremental GC : Le GC s'exécute par petites portions entre les instructions du programme pour réduire les "pauses" (moins de freeze de l'UI).

Détail méconnu : Le Garbage Collector de V8 (Orinoco) utilise une approche parallèle et concurrente : il peut collecter la mémoire en arrière-plan pendant que le code s'exécute, réduisant considérablement les pauses.

1.7 Single Thread & Event Loop

Définition : JavaScript est un langage mono-thread (single-threaded), ce qui signifie qu'il ne peut exécuter qu'une seule tâche à la fois sur un seul thread (le main thread). Pour gérer les opérations asynchrones (timers, requêtes HTTP, événements), JavaScript utilise l'Event Loop.

Métaphore : Imaginez un serveur de café avec un seul barista (le thread principal). Quand un client commande (événement), le barista peut :

  • Préparer le café immédiatement (synchrone).
  • Mettre la commande dans une file d'attente (Callback Queue) et servir les autres clients en attendant (asynchrone).
  • La machine à café (Web APIs) prépare le café en arrière-plan. Quand c'est prêt, elle ajoute la tâche dans la file d'attente.

L'Event Loop : Le barista (Event Loop) vérifie constamment :

  • Si la Call Stack est vide.
  • Si oui, il prend la première tâche de la file d'attente et l'exécute.
  • Si non, il attend.

Les composants de l'Event Loop

Composant Rôle Exemple
Call Stack Exécute le code synchrone. console.log('Hello')
Web APIs Fonctions asynchrones fournies par le navigateur. setTimeout, fetch, addEventListener
Callback Queue File d'attente des callbacks prêts à être exécutés. Callbacks de setTimeout, de fetch
Event Loop Transfère les callbacks de la queue vers la Call Stack. Boucle infinie
Microtask Queue File d'attente prioritaire (Promise, queueMicrotask). Promise.then()

Exemple Concret d'Exécution

Analysons ce code :

console.log('1. Début');

setTimeout

(() => { console.log('2. Timer 0ms'); }, 0);

Promise.resolve().then(() => console.log('3. Promise'));

console.log('4. Fin');

Déroulement :

  1. console.log('1. Début') → Exécuté immédiatement sur la Call Stack.
  2. setTimeout(..., 0) → Le callback est envoyé aux Web APIs. Le timer expire presque immédiatement, le callback est placé dans la Callback Queue.
  3. Promise.resolve().then(...) → Le callback est placé dans la Microtask Queue (priorité plus élevée).
  4. console.log('4. Fin') → Exécuté immédiatement sur la Call Stack.
  5. La Call Stack est vide. L'Event Loop vide d'abord la Microtask Queue → '3. Promise' est affiché.
  6. Ensuite, l'Event Loop vide la Callback Queue → '2. Timer 0ms' est affiché.

Résultat final :

1. Début
4. Fin
3. Promise
2. Timer 0ms

Détail méconnu : Les Microtasks (Promise, queueMicrotask) ont la priorité sur les Macrotasks (setTimeout, setInterval, I/O). C'est pourquoi la Promise s'exécute avant le setTimeout(0) dans l'exemple ci-dessus.

1.8 Synthèse - Cycle de Vie d'un Programme JS

┌─────────────────────────────────────────────────────────────────┐

│ CYCLE DE VIE D'UN PROGRAMME JS │

│ │

│ 📄 Code Source │

│ ↓ │

│ 🔍 Parsing (Scanning → Tokens → AST) │

│ ↓ │

│ ⚙️ Bytecode (Génération par l'interpréteur) │

│ ↓ │

│ 🚀 Exécution sur la Call Stack │

│ ↓ │

│ 🔥 Profilage & Compilation JIT (TurboFan) │

│ ↓ │

│ ⚡ Exécution Optimisée (Code Machine) │

│ ↓ │

│ 🗑️ Garbage Collector (Nettoyage mémoire) │

│ │

│ 🔄 L'Event Loop gère les opérations asynchrones │

│ 📥 Callback Queue → Event Loop → Call Stack │

└─────────────────────────────────────────────────────────────────┘

Concept Définition Rôle
Parsing Analyse syntaxique du code source en tokens puis en AST. Vérifier la syntaxe, structurer le code.
AST Arbre Syntaxique Abstrait (représentation arborescente du code). Base pour la génération de bytecode et les optimisations.
Bytecode Instructions intermédiaires compactes. Exécution rapide par l'interpréteur (Ignition).
JIT Compilation Just-In-Time (Traduction en code machine). Accélérer les fonctions "chaudes" (TurboFan).
Call Stack Pile d'appels des fonctions en cours. Suivre l'ordre d'exécution, stocker le contexte.
Heap Tasse mémoire (stockage des objets, tableaux, fonctions). Stocker les données de taille variable.
Garbage Collector Libération automatique de la mémoire inutilisée. Éviter les fuites de mémoire.
Event Loop Boucle d'événements (gestion des opérations asynchrones). Transférer les callbacks de la queue vers la Call Stack.
Microtask Queue File d'attente prioritaire (Promise, queueMicrotask). Exécuter les callbacks prioritaires avant les macrotasks.
Macrotask Queue File d'attente des tâches (timers, I/O). Exécuter les callbacks après les microtasks.