MFormations
Modern Frontend Engineering

Chapitre 5

Chapitre 05 — JavaScript : Le Langage du Web

Chapitre 05 — JavaScript : Le Langage du Web

Chapitre 05 — JavaScript : Cours Complet

1. Histoire d'ECMAScript

1.1 De Mocha à ECMAScript

JavaScript est créé en 10 jours par Brendan Eich chez Netscape en mai 1995. Nommé Mocha, puis LiveScript, puis JavaScript (marketing). En 1996, Netscape soumet le langage à l'ECMA pour standardisation.

Diagramme en cours de génération...

1.2 Le Cycle de Release Annuel

Depuis ES2015, une nouvelle version d'ECMAScript est publiée chaque année en juin. Chaque proposition passe par 4 stages avant d'être incluse :

  • Stage 1 : Proposal (idée)
  • Stage 2 : Draft (première spécification)
  • Stage 3 : Candidate (implémentations navigateurs)
  • Stage 4 : Finished (inclus dans la spec)

2. Types : Primitifs vs Objets

2.1 Les 7 Types Primitifs

// Primitifs (immutables, passés par valeur)
undefined
null
boolean
number
string
symbol     // ES2015
bigint     // ES2020

// Objets (mutables, passés par référence)
Object
Array
Function
Date
RegExp
Map, Set, WeakMap, WeakSet

2.2 typeof et instanceof

typeof undefined      // "undefined"
typeof null           // "object" (bug historique depuis 1995)
typeof true           // "boolean"
typeof 42             // "number"
typeof "hello"        // "string"
typeof Symbol()       // "symbol"
typeof 42n            // "bigint"
typeof {}             // "object"
typeof []             // "object"
typeof function(){}   // "function"

// instanceof pour les objets
[] instanceof Array       // true
{} instanceof Object      // true
(function(){}) instanceof Function  // true

// ⚠️ Piège : typeof null === "object"
// Solution :
function isNull(v) { return v === null; }

// ⚠️ typeof NaN === "number"
Number.isNaN(NaN)  // true (Array.isArray-like)

2.3 Conversion de Type (Coercion)

// Coercion implicite (source de bugs)
1 + "2"           // "12"
"5" - 3           // 2 (string → number automatique)
!"hello"          // false
!!"hello"         // true
[] + []           // "" (string vide)
{} + []           // 0 ({} est un bloc, +[] = 0)

// Coercion explicite (toujours préférer)
String(42)          // "42"
Number("42")        // 42
Boolean("hello")    // true
parseInt("42px", 10) // 42
+"42"               // 42 (unary operator)

3. Closures

3.1 Mécanisme Interne

Une closure est la combinaison d'une fonction et de son environnement lexical (scope) au moment de sa création.

function creerCompteur() {
  let count = 0;  // variable dans le scope de creerCompteur

  return function() {
    count++;      // closure : capture count
    return count;
  };
}

const compteurA = creerCompteur();
const compteurB = creerCompteur();

compteurA(); // 1
compteurA(); // 2
compteurB(); // 1 (indépendant)
Diagramme en cours de génération...

3.2 Cas d'Usage

// 1. Factory functions
function createLogger(prefix) {
  return function(message) {
    console.log(`[${prefix}] ${message}`);
  };
}
const info = createLogger('INFO');
const error = createLogger('ERROR');
info('Application démarrée'); // [INFO] Application démarrée

// 2. Module pattern (IIFE)
const counter = (function() {
  let count = 0;
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
})();

// 3. Event listeners
function attachClickHandler(id) {
  document.getElementById(id).addEventListener('click', () => {
    console.log(`Click on ${id}`); // closure sur id
  });
}

3.3 Pièges des Closures

// ❌ Problème classique dans les boucles (var)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// ✅ Solution 1 : let (block scope)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// ✅ Solution 2 : IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}

// ✅ Solution 3 : closure factory
for (var i = 0; i < 3; i++) {
  setTimeout(createLog(i), 100);
}
function createLog(n) {
  return () => console.log(n);
}

3.4 Memory Leaks avec Closures

function createLeak() {
  const heavyData = new Array(1000000).fill('data');
  return function() {
    console.log('hello');
    // heavyData est capturé mais jamais utilisé !
    // Le GC ne peut pas libérer heavyData tant que cette closure existe
  };
}

4. Scope : Global, Function, Block

// Global scope
const globalVar = 'accessible partout';

function scopeDemo() {
  // Function scope
  var functionVar = 'accessible dans la fonction';

  if (true) {
    // Block scope (let, const)
    let blockLet = 'block scope';
    const blockConst = 'block scope';
    var functionVar2 = 'pas de block scope pour var';
  }

  console.log(functionVar2); // OK
  // console.log(blockLet);   // ReferenceError
}

// Lexical scoping : la portée est déterminée par la position dans le code source
const x = 'global';

function outer() {
  const x = 'outer';

  function inner() {
    const x = 'inner';
    console.log(x); // 'inner'
  }

  inner();
}
outer();

5. Hoisting

// var : hoistée avec valeur undefined
console.log(a); // undefined (pas d'erreur !)
var a = 5;

// let/const : hoistées mais pas initialisées (Temporal Dead Zone)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;

// Function declarations : hoistées complètement
hello(); // "Hello!"
function hello() {
  console.log("Hello!");
}

// Function expressions : hoistées comme var
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
  console.log("Hi!");
};

// TDZ illustrée
{
  // Début de la TDZ pour 'x'
  const func = () => console.log(x);
  let x = 3; // Fin de la TDZ
  func(); // 3 (car func est appelé APRÈS la déclaration)
}
Diagramme en cours de génération...

6. Event Loop

6.1 Architecture

Diagramme en cours de génération...

6.2 Ordre d'Exécution

console.log('1: sync');

setTimeout(() => console.log('2: setTimeout'), 0);

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

queueMicrotask(() => console.log('4: queueMicrotask'));

requestAnimationFrame(() => console.log('5: rAF'));

console.log('6: sync');

// Output :
// 1: sync
// 6: sync
// 3: Promise microtask
// 4: queueMicrotask
// 2: setTimeout  (après toutes les microtasks)
// 5: rAF (avant le prochain paint)

6.3 Règles Importantes

  1. Le Call Stack doit être vide avant de traiter les queues
  2. Les microtasks sont vidées complètement avant chaque task
  3. Une microtask qui en ajoute une autre retarde la prochaine task
  4. requestAnimationFrame s'exécute avant le paint, après les microtasks
// ⚠️ Microtask infinite loop (bloque le navigateur)
function loop() {
  Promise.resolve().then(loop);
}
loop(); // ❌ Les tasks (click, setTimeout) ne s'exécuteront jamais

7. this : Contexte d'Exécution

7.1 Les 5 Règles de this

// 1. 🔵 Default binding (strict mode)
function showThis() {
  'use strict';
  console.log(this); // undefined
}
showThis();

// 2. 🔵 Default binding (non-strict)
function showThis() {
  console.log(this); // globalThis (window)
}

// 3. 🟢 Implicit binding (objet appelant)
const obj = {
  name: 'obj',
  show() { console.log(this.name); }
};
obj.show(); // 'obj'
const detached = obj.show;
detached(); // undefined ou window (perte du contexte)

// 4. 🟡 Explicit binding (call, apply, bind)
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}
const user = { name: 'Alice' };
greet.call(user, 'Hello');   // Hello, Alice
greet.apply(user, ['Hi']);   // Hi, Alice
const bound = greet.bind(user);
bound('Hey');                 // Hey, Alice

// 5. 🔴 new binding (constructeur)
function Person(name) {
  this.name = name;
}
const alice = new Person('Alice');

// 6. 🟣 Arrow functions (lexical this, pas de binding)
const arrow = {
  name: 'arrow',
  show: () => console.log(this.name), // this = window/undefined
  showRegular() {
    const inner = () => console.log(this.name); // this = objet
    inner();
  }
};

7.2 Perte de Contexte et Solutions

class Button {
  constructor(text) {
    this.text = text;
    // Solution 1 : bind dans constructor
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this.text);
  }

  // Solution 2 : arrow function dans class
  handleClickArrow = () => {
    console.log(this.text);
  };
}

// Solution 3 : closure dans event listener
button.addEventListener('click', () => instance.handleClick());

8. Prototypes vs Classes

8.1 Héritage Prototypal

// Chaque objet a un prototype (sauf Object.create(null))
const parent = { name: 'parent' };
const child = Object.create(parent);
child.age = 10;

console.log(child.name); // 'parent' (via prototype)
console.log(child.age);  // 10 (propre)

// La chaîne de prototypes
console.log(child.__proto__ === parent);           // true
console.log(parent.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);            // null
Diagramme en cours de génération...

8.2 Constructeurs (Pré-ES6)

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  return `${this.name} fait du bruit`;
};

function Dog(name, breed) {
  Animal.call(this, name); // Appel du constructeur parent
  this.breed = breed;
}

// Héritage du prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  return `${this.name} (${this.breed}) aboie`;
};

const rex = new Dog('Rex', 'Berger');
rex.speak(); // "Rex (Berger) aboie"

8.3 Classes ES6 (Syntax Sugar)

class Animal {
  #privateField = 'privé';  // Private fields (ES2022)

  constructor(name) {
    this.name = name;
  }

  // Méthode de prototype
  speak() {
    return `${this.name} fait du bruit`;
  }

  // Méthode statique
  static isAnimal(obj) {
    return obj instanceof Animal;
  }

  // Getter
  get description() {
    return `Animal: ${this.name}`;
  }

  // Méthode privée
  #privateMethod() {
    return `Secret: ${this.#privateField}`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Obligatoire avant d'utiliser this
    this.breed = breed;
  }

  speak() {
    return `${super.speak()} → aboie`;
  }
}

9. Async : Callbacks → Promises → async/await

9.1 L'Évolution

Diagramme en cours de génération...

9.2 Promises en Détail

// Création
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) resolve('Données');
    else reject(new Error('Échec'));
  }, 1000);
});

// Consommation
promise
  .then(data => {
    console.log(data);
    return process(data);
  })
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Terminé'));

// Combinators
Promise.all([fetch('/a'), fetch('/b')])     // Toutes réussissent
Promise.allSettled([fetch('/a'), fetch('/b')]) // Toutes, échouées ou non
Promise.race([fetch('/a'), fetch('/b')])    // La première terminée
Promise.any([fetch('/a'), fetch('/b')])     // La première réussie (ES2021)
Promise.withResolvers()                      // ES2024

9.3 async/await

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Erreur fetch:', error);
    throw error; // Re-lancer pour le caller
  }
}

// Top-level await (ES2022) : dans les modules
const config = await fetch('/config.json').then(r => r.json());

// Parallel execution
async function parallel() {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  return { users, posts };
}

// Sequential vs parallel
async function sequence() {
  const a = await fetch('/api/a');  // 1s
  const b = await fetch('/api/b');  // 1s (attend a)
  // Total : 2s
}

async function parallel2() {
  const [a, b] = await Promise.all([
    fetch('/api/a'),  // 1s
    fetch('/api/b')   // 1s (en parallèle)
  ]);
  // Total : 1s
}

10. Modules : ES Modules, CommonJS, Dynamic Imports

// math.js - ES Module (ES2015)
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export default class Calculator {
  // ...
}

// app.js - Import
import Calculator, { PI, add } from './math.js';

// Dynamic import (ES2020)
const module = await import('./math.js');
console.log(module.PI);

// CommonJS (Node.js legacy)
// math.cjs
const PI = 3.14159;
module.exports = { PI };
// app.cjs
const { PI } = require('./math.cjs');
AspectES ModulesCommonJS
Syntaxeimport/exportrequire()/module.exports
Statique✅ Analyse statique❌ Dynamique
Tree-shaking✅ Possible❌ Impossible
Cycle✅ Géré❌ Problématique
Top-level await✅ Oui❌ Non
Navigateur✅ Natif❌ Bundler nécessaire

11. Gestion Mémoire

11.1 V8 Garbage Collector

Diagramme en cours de génération...
  • Young Generation : objets récents, collectés fréquemment (Scavenger)
  • Old Generation : objets qui survivent, collectés moins souvent (Mark-Sweep-Compact)

11.2 Memory Leaks Courants

// 1. Variables globales involontaires
function leak() {
  leaked = 'devient global'; // pas de var/let/const
}

// 2. Closures qui capturent trop
function createLeak() {
  const bigData = new Array(1000000);
  return () => console.log('small');
}

// 3. Timers non nettoyés
const interval = setInterval(() => {
  const heavy = new Array(100000);
  console.log(heavy.length);
}, 100);
// clearInterval(interval) manquant

// 4. Event listeners non détachés
class Component {
  constructor() {
    window.addEventListener('resize', this.handleResize);
    // ❌ Jamais de removeEventListener
  }
}

// 5. Références DOM orphelines
const elements = [];
const div = document.createElement('div');
elements.push(div);
document.body.removeChild(div);
// elements[0] garde une référence → pas de GC

// ✅ Solutions
// - Utiliser WeakRef ou FinalizationRegistry si nécessaire
// - Toujours nettoyer les timers et listeners
// - Éviter les globaux implicites
// - Utiliser les outils Chrome Memory/Performance

12. Proxy, Reflect, Symbol, Generators

12.1 Proxy et Reflect

const handler = {
  get(target, prop, receiver) {
    if (prop in target) {
      return Reflect.get(target, prop, receiver);
    }
    return `Propriété "${String(prop)}" inexistante`;
  },

  set(target, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('age doit être un nombre');
    }
    return Reflect.set(target, prop, value);
  },

  deleteProperty(target, prop) {
    console.log(`Suppression de ${String(prop)}`);
    return Reflect.deleteProperty(target, prop);
  }
};

const person = new Proxy({ name: 'Alice' }, handler);
console.log(person.name);      // 'Alice'
console.log(person.unknown);   // 'Propriété "unknown" inexistante'
person.age = 30;               // OK
// person.age = 'trente';      // TypeError

12.2 Symbol

const UNIQUE = Symbol('description');
const obj = {
  [UNIQUE]: 'valeur privée',
  regular: 'publique'
};

Object.keys(obj);                    // ['regular']
Object.getOwnPropertySymbols(obj);   // [Symbol(description)]

// Symboles well-known
const iterable = {
  [Symbol.iterator]: function* () {
    yield 1; yield 2; yield 3;
  }
};

// Métaprogrammation
class MyClass {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}
[] instanceof MyClass; // true

12.3 Generators et Iterators

// Generator function
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
fib.next(); // { value: 0, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 1, done: false }

// Iterator personnalisé
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

for (const n of new Range(1, 5)) {
  console.log(n); // 1, 2, 3, 4, 5
}

13. Design Patterns en JavaScript

13.1 Module Pattern

// Module Pattern (IIFE)
const UserModule = (function() {
  const users = new Map();
  let idCounter = 0;

  function validateEmail(email) {
    return email.includes('@');
  }

  return {
    addUser(name, email) {
      if (!validateEmail(email)) throw new Error('Email invalide');
      const id = ++idCounter;
      users.set(id, { id, name, email });
      return id;
    },
    getUser(id) {
      return users.get(id);
    },
    getAllUsers() {
      return Array.from(users.values());
    }
  };
})();

13.2 Observer Pattern

class EventEmitter {
  constructor() {
    this._events = new Map();
  }

  on(event, listener) {
    if (!this._events.has(event)) {
      this._events.set(event, []);
    }
    this._events.get(event).push(listener);
    return () => this.off(event, listener); // unsubscribe
  }

  off(event, listener) {
    const listeners = this._events.get(event);
    if (listeners) {
      this._events.set(event, listeners.filter(l => l !== listener));
    }
  }

  emit(event, ...args) {
    const listeners = this._events.get(event);
    if (listeners) {
      listeners.forEach(listener => listener(...args));
    }
  }
}

13.3 Singleton Pattern

class Database {
  static #instance;

  constructor() {
    if (Database.#instance) {
      return Database.#instance;
    }
    this.connection = null;
    Database.#instance = this;
  }

  static getInstance() {
    if (!Database.#instance) {
      Database.#instance = new Database();
    }
    return Database.#instance;
  }
}

13.4 Factory Pattern

class ButtonFactory {
  createButton(type, options) {
    switch (type) {
      case 'primary':
        return new PrimaryButton(options);
      case 'secondary':
        return new SecondaryButton(options);
      case 'danger':
        return new DangerButton(options);
      default:
        throw new Error(`Type inconnu: ${type}`);
    }
  }
}

14. Erreurs des Juniors

// ❌ Hoisting trap avec var
var x = 5;
(function() {
  console.log(x); // undefined (hoisted var, pas la globale)
  var x = 10;
})();

// ✅ Avec let (ReferenceError claire)
(function() {
  console.log(y); // ReferenceError (TDZ)
  let y = 10;
})();

// ❌ Closure leak dans boucle
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100); // 5, 5, 5, 5, 5
}

// ✅ let dans la boucle
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2, 3, 4
}

// ❌ Async mistake : pas de await dans forEach
async function wrong(items) {
  items.forEach(async item => {
    await process(item); // ⚠️ Pas attendu !
  });
  console.log('Fini'); // S'exécute avant !
}

// ✅ Solution : for...of
async function right(items) {
  for (const item of items) {
    await process(item);
  }
  console.log('Fini'); // Après tout
}

// ❌ Mutation non intentionnelle
const arr = [1, 2, 3];
const copy = arr;
copy.push(4);
console.log(arr); // [1, 2, 3, 4] ⚠️

// ✅ Copie réelle
const copy2 = [...arr];
const copy3 = structuredClone(arr);

// ❌ Comparaison d'objets
{a: 1} === {a: 1} // false
// ✅ Comparaison en profondeur
JSON.stringify({a: 1}) === JSON.stringify({a: 1}) // true (attention aux limites)

Résumé

JavaScript moderne est un langage mature et puissant. Un ingénieur front-end doit maîtriser :

  1. Types et coercion (connaître les pièges)
  2. Closures (mécanisme + memory leaks)
  3. Scope et hoisting (var vs let/const, TDZ)
  4. Event Loop (tasks, microtasks, rAF)
  5. this (les 5 règles + arrow functions)
  6. Prototypes/Classes (héritage prototypal)
  7. Async (Promises, async/await, error handling)
  8. Modules (ESM, CommonJS, dynamic imports)
  9. Memory (GC V8, leaks)
  10. Design patterns (Module, Observer, Singleton, Factory)