Cours JavaScript et TypeScript · Module 12: Programmation fonctionnelle
Motifs de données immuables
Dans cette leçon7
La carte du parc et le module des rapports reçoivent le même objet d'état. La carte déplace Rex vers la zone B en modifiant directement le champ, et soudain, le rapport de l'équipe du matin affiche lui aussi la zone B, alors que le matin, Rex était en A. Un objet partagé modifié à un endroit change partout où quelqu'un garde une référence vers lui.
Dans les laboratoires de Jurassic Park règne une règle stricte : on ne modifie jamais l'échantillon d'ADN original. Chaque modification crée une nouvelle copie. En programmation fonctionnelle, on applique la même approche aux données : les structures originales restent intactes, et chaque modification crée un nouvel objet.
L'opérateur spread - Copier et étendre
L'opérateur spread (...) est l'outil de base pour créer des copies immuables d'objets et de tableaux. Commençons par déplacer un dinosaure vers une autre zone :
1// Copie d'un objet avec modification
2const dinosaur = { name: 'Rex', health: 100, zone: 'A' };
3const movedDino = { ...dinosaur, zone: 'B' };
4// dinosaur: { name: 'Rex', health: 100, zone: 'A' } - inchangé
5// movedDino: { name: 'Rex', health: 100, zone: 'B' } - nouvel objetLes champs écrits après le spread écrasent ceux qui ont été copiés, donc zone: 'B' l'emporte sur zone: 'A'. movedDino et dinosaur sont deux objets différents, et le rapport qui garde dinosaur voit toujours la zone A.
On procède de la même façon avec les tableaux : on ajoute un élément avec le spread, et on le retire avec filter :
1// Copie d'un tableau avec ajout d'un élément
2const zones = ['A', 'B', 'C'];
3const extendedZones = [...zones, 'D'];
4// zones: ['A', 'B', 'C'] - inchangé
5// extendedZones: ['A', 'B', 'C', 'D']
6
7// Suppression d'un élément d'un tableau (sans mutation)
8const dinosaurs = ['Rex', 'Blue', 'Brachio', 'Stego'];
9const withoutBlue = dinosaurs.filter((d) => d !== 'Blue');
10// dinosaurs: ['Rex', 'Blue', 'Brachio', 'Stego'] - inchangé
11// withoutBlue: ['Rex', 'Brachio', 'Stego']Aucune de ces opérations n'a utilisé push ni splice, qui modifient le tableau sur place.
Objets imbriqués - L'immutabilité en profondeur
L'opérateur spread crée une copie superficielle - les objets imbriqués restent partagés ! Tu dois utiliser le spread à chaque niveau. Voici l'état du parc, avec les zones à l'intérieur, et une tentative de copie en un seul spread :
1const park = {
2 name: 'Jurassic Park',
3 zones: {
4 A: { dinosaurs: ['Rex'], voltage: 10000 },
5 B: { dinosaurs: ['Brachio'], voltage: 5000 },
6 },
7};
8
9// MAUVAIS - copie superficielle, zones est partagé
10const badCopy = { ...park };
11badCopy.zones.A.voltage = 0; // MODIFIE aussi l'original !badCopy est un nouvel objet, mais son champ zones pointe vers le même objet que park.zones. Modifier la tension via la copie revient donc à modifier l'original, exactement comme dans l'histoire de la carte et du rapport.
Une mise à jour correcte copie chaque niveau sur le chemin qui mène au champ modifié. On suppose ici un objet park tout neuf, car la ligne avec badCopy a déjà remis l'original à zéro :
1// BON - copie profonde à chaque niveau
2const updatedPark = {
3 ...park,
4 zones: {
5 ...park.zones,
6 A: {
7 ...park.zones.A,
8 voltage: 15000,
9 },
10 },
11};
12// park.zones.A.voltage: 10000 (inchangé)
13// updatedPark.zones.A.voltage: 15000 (nouvel objet)Seuls les objets situés sur le chemin sont nouveaux : updatedPark, son zones et la zone A. La zone B n'a pas été copiée, donc updatedPark.zones.B === park.zones.B. C'est normal, car nous ne l'avons pas modifiée, et partager les parties inchangées économise de la mémoire.
Object.freeze() - Geler les objets
Object.freeze() empêche la modification d'un objet au runtime. Une tentative de modification est silencieusement ignorée, et en mode strict (strict mode), elle lève une TypeError. Chaque module ES et chaque classe s'exécutent en mode strict :
1const CONFIG = Object.freeze({
2 maxDinosaurs: 100,
3 maxVoltage: 50000,
4 alertThreshold: 3,
5});
6
7CONFIG.maxDinosaurs = 200; // Ignoré ! (ou TypeError en strict mode)
8console.log(CONFIG.maxDinosaurs); // toujours 100Object.freeze renvoie le même objet, simplement gelé : impossible de modifier, d'ajouter ou de supprimer ses champs. Il y a cependant un piège, visible avec un objet à structure imbriquée :
1// ATTENTION : freeze est superficiel !
2const deepConfig = Object.freeze({
3 zones: { A: { limit: 10 } },
4});
5deepConfig.zones.A.limit = 999; // ÇA MARCHE - freeze est superficielSeul le premier niveau est gelé. L'objet zones est resté un objet ordinaire, donc pour tout geler, il faut appeler récursivement Object.freeze sur chaque objet imbriqué.
const, ce n'est pas freeze
Un malentendu fréquent : puisque CONFIG est déclaré avec const, pourquoi ajouter encore freeze ? Parce que ces deux mécanismes protègent des choses complètement différentes :
1const zone = { id: 'A', voltage: 10000 };
2zone.voltage = 0; // fonctionne - const ne protège pas le contenu
3// zone = { id: 'B' }; // TypeError - const protège l'affectation
4
5const lockedZone = Object.freeze({ id: 'A', voltage: 10000 });
6lockedZone.voltage = 0; // ignoré (TypeError en strict mode)const bloque la réaffectation de la variable, c'est-à-dire la référence. Object.freeze bloque la modification des valeurs à l'intérieur de l'objet. Protège une configuration constante avec les deux à la fois, comme CONFIG.
structuredClone() - La copie profonde
structuredClone() crée une copie profonde d'un objet - toutes les structures imbriquées sont indépendantes. C'est pratique quand tu modifies beaucoup de choses en profondeur dans une structure :
1const original = {
2 name: 'Jurassic Park',
3 zones: [
4 { id: 'A', dinosaurs: [{ name: 'Rex', health: 100 }] },
5 { id: 'B', dinosaurs: [{ name: 'Blue', health: 95 }] },
6 ],
7};
8
9const deepCopy = structuredClone(original);
10deepCopy.zones[0].dinosaurs[0].health = 50;
11
12console.log(original.zones[0].dinosaurs[0].health); // 100 (inchangé !)
13console.log(deepCopy.zones[0].dinosaurs[0].health); // 50 (copie indépendante)La modification de la copie n'a pas atteint l'original. structuredClone fonctionne dans les navigateurs modernes et dans Node.js depuis la version 17, mais il ne copie pas les fonctions (il lève une DataCloneError), et il transforme les instances de classes en objets ordinaires sans méthodes. Mon conseil : au quotidien, copie avec le spread uniquement le chemin que tu modifies, et garde structuredClone pour les moments où tu as besoin d'une copie des données totalement indépendante.
Mises à jour immuables des tableaux
Voici l'ensemble complet des opérations sur les tableaux sans mutation, réunies sur une seule liste de dinosaures :
1const dinosaurs = [
2 { id: 1, name: 'Rex', health: 100 },
3 { id: 2, name: 'Blue', health: 95 },
4 { id: 3, name: 'Brachio', health: 88 },
5];
6
7// Ajout
8const added = [...dinosaurs, { id: 4, name: 'Stego', health: 100 }];
9
10// Suppression
11const removed = dinosaurs.filter((d) => d.id !== 2);
12
13// Mise à jour
14const updated = dinosaurs.map((d) =>
15 d.id === 1 ? { ...d, health: 80 } : d
16);
17
18// Tri (sans mutation - on crée une copie avant sort)
19const sorted = [...dinosaurs].sort((a, b) => a.health - b.health);La mise à jour via map avec l'opérateur conditionnel est un motif que tu verras partout : l'élément modifié reçoit un nouvel objet, les autres passent sans changement. On copie le tableau avant sort, car sort le modifie sur place. Depuis ES2023, tu disposes aussi d'équivalents tout prêts sans mutation : toSorted, toReversed, toSpliced et with.
Des techniques simples aux bibliothèques
Rangeons les outils, du plus simple au plus complet : Object.freeze veille sur un seul niveau, le spread fait une copie superficielle avec modification, et structuredClone une copie profonde. Pour les grosses structures, on se tourne vers des bibliothèques. Immutable.js fournit des structures de données persistantes (persistent data structures) : chaque « modification » renvoie une nouvelle version, qui partage les fragments inchangés avec la précédente. Immer permet d'écrire des affectations ordinaires sur un brouillon, puis crée lui-même une nouvelle version immuable. Le même motif t'attend dans le monde de React, où l'état se met à jour avec une nouvelle copie, et non par mutation.
Retiens : on ne touche jamais à l'échantillon d'ADN original - chaque modification, c'est une nouvelle éprouvette, et le spread, structuredClone et Object.freeze font respecter cette règle à différents niveaux.
Code de cette leçon : index.js
1// Motifs d'immutabilite des donnees
2console.log("=== Jurassic Park - Protection des Echantillons Originaux ===\n");
3
4// --- EXEMPLE 1 : Spread operator ---
5console.log("--- Spread: objets ---");
6const dino = { name: "Rex", health: 100, zone: "A" };
7const moved = { ...dino, zone: "B" };
8console.log("Original:", dino);
9console.log("Deplace:", moved);
10
11console.log("\n--- Spread: tableaux ---");
12const zones = ["A", "B", "C"];
13const extended = [...zones, "D"];
14console.log("Originaux:", zones);
15console.log("Etendus:", extended);
16
17// --- EXEMPLE 2 : Objets imbriques ---
18console.log("\n--- Objets imbriques ---");
19const park = {
20 name: "Jurassic Park",
21 zones: {
22 A: { dinosaurs: ["Rex"], voltage: 10000 },
23 B: { dinosaurs: ["Brachio"], voltage: 5000 },
24 },
25};
26
27const updatedPark = {
28 ...park,
29 zones: {
30 ...park.zones,
31 A: { ...park.zones.A, voltage: 15000 },
32 },
33};
34
35console.log("Original A voltage:", park.zones.A.voltage);
36console.log("Updated A voltage:", updatedPark.zones.A.voltage);
37console.log("Original B voltage:", park.zones.B.voltage, "(inchange)");
38
39// --- EXEMPLE 3 : Object.freeze ---
40console.log("\n--- Object.freeze ---");
41const CONFIG = Object.freeze({
42 maxDinosaurs: 100,
43 maxVoltage: 50000,
44});
45
46CONFIG.maxDinosaurs = 200; // Ignore !
47console.log("maxDinosaurs:", CONFIG.maxDinosaurs, "(toujours 100)");
48
49// --- EXEMPLE 4 : structuredClone ---
50console.log("\n--- structuredClone ---");
51const original = {
52 zones: [{ id: "A", dinos: [{ name: "Rex", hp: 100 }] }],
53};
54const deepCopy = structuredClone(original);
55deepCopy.zones[0].dinos[0].hp = 50;
56
57console.log("Original hp:", original.zones[0].dinos[0].hp, "(inchange !)");
58console.log("Copie hp:", deepCopy.zones[0].dinos[0].hp);
59
60// --- EXEMPLE 5 : Operations immuables sur les tableaux ---
61console.log("\n--- Operations immuables ---");
62const dinosaurs = [
63 { id: 1, name: "Rex", health: 100 },
64 { id: 2, name: "Blue", health: 95 },
65 { id: 3, name: "Brachio", health: 88 },
66];
67
68// Ajout
69const added = [...dinosaurs, { id: 4, name: "Stego", health: 100 }];
70console.log("Ajoute (nouveau tableau):", added.length, "items");
71
72// Suppression
73const removed = dinosaurs.filter((d) => d.id !== 2);
74console.log("Blue supprime:", removed.map((d) => d.name));
75
76// Mise a jour
77const updated = dinosaurs.map((d) =>
78 d.id === 1 ? { ...d, health: 80 } : d
79);
80console.log("Updated Rex:", updated[0].health);
81console.log("Original Rex:", dinosaurs[0].health, "(inchange !)");
82
83// Tri (immuable)
84const sorted = [...dinosaurs].sort((a, b) => a.health - b.health);
85console.log("Trie:", sorted.map((d) => d.name + ":" + d.health));
86console.log("Original:", dinosaurs.map((d) => d.name + ":" + d.health));Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que fait le spread operator (...) avec les objets imbriqués ?
2. Quelle méthode de copie d'objet en JavaScript crée un clone profond ?
Tâches pratiques dans le jeu
- Éditeur de code
Ajoute, supprime et mets à jour des données dans des objets imbriqués à l'aide du spread operator et de filter/map.
- Tri vertical
Ordonne les techniques d'immutabilité des données, de la plus simple à la plus avancée :
- Éditeur de code
Construis un pipeline avec validation, classification des alertes, regroupement et statistiques.
- Clique dans l'ordre
Ordonne les éléments du gel d'un objet de configuration :
- Tri horizontal
Ordonne les éléments d'une mise à jour immuable d'un élément de tableau :
- Clique dans l'ordre
Ordonne les éléments de l'ajout immuable d'un élément à un tableau :