Cours JavaScript et TypeScript · Module 2: Fonctions, tableaux et objets
Les opérateurs spread et rest
Dans cette leçon6
À Jurassic Park, il faut sans cesse fusionner les données de différents systèmes de surveillance, copier des jeux de paramètres et gérer des fonctions qui reçoivent tantôt deux, tantôt dix échantillons. Les opérateurs spread (...) et rest (...) partagent la même écriture, trois points, mais jouent deux rôles opposés. Le spread "déballe" des éléments, et le rest les "emballe" dans une seule structure. "C'est le système de transport du parc", dit le Dr Rex. "Le spread décharge les conteneurs d'échantillons sur des postes séparés, et le rest emballe de nombreux échantillons dans un seul conteneur."
L'opérateur spread pour les tableaux
Le spread "étale" les éléments d'un tableau là où l'on attend des valeurs individuelles. Voici comment fusionner la liste des carnivores et celle des herbivores :
1const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
2const herbivores = ['Triceratops', 'Brachiosaurus', 'Stegosaurus'];
3
4// Fusionner des tableaux
5const allDinos = [...carnivores, ...herbivores];
6console.log(allDinos);
7// ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Triceratops', 'Brachiosaurus', 'Stegosaurus']
8
9// Ajouter des éléments pendant la fusion
10const withNew = ['Pteranodon', ...carnivores, 'Dilophosaurus'];
11console.log(withNew);
12// ['Pteranodon', 'T-Rex', 'Velociraptor', 'Spinosaurus', 'Dilophosaurus']Le résultat est un seul tableau plat, et non un tableau de tableaux, car le spread insère les éléments et pas les tableaux entiers. L'écriture [carnivores, herbivores] sans les points donnerait deux conteneurs imbriqués. Les deux tableaux sources restent d'ailleurs inchangés.
Copier un tableau (shallow copy)
Le spread crée une copie superficielle du tableau, c'est-à-dire un nouveau tableau avec les mêmes éléments. L'écriture est const copy = [...original] :
1const originalZones = ['A', 'B', 'C'];
2const zonesCopy = [...originalZones];
3
4zonesCopy.push('D');
5console.log(originalZones); // ['A', 'B', 'C'] - l'original est inchangé !
6console.log(zonesCopy); // ['A', 'B', 'C', 'D']Attention, copie superficielle ! Si le tableau contient des objets, seules les références vers ces objets sont copiées, pas les objets eux-mêmes :
1const dinos = [{ name: 'Rex', health: 100 }];
2const dinosCopy = [...dinos];
3
4dinosCopy[0].health = 50;
5console.log(dinos[0].health); // 50 - l'original a changé lui aussi !
6// Parce que les deux éléments pointent vers le même objet en mémoireSeul le conteneur extérieur est neuf, les objets imbriqués partagent toujours leurs références avec l'original. Quand tu as besoin d'une copie profonde et indépendante, je te recommande la fonction intégrée structuredClone().
Le spread avec les fonctions
Le spread permet aussi de "déballer" un tableau dans les arguments d'une fonction qui attend des valeurs séparées, comme Math.max :
1const temperatures = [36.5, 37.2, 38.1, 35.8, 39.0];
2
3// Au lieu de Math.max(36.5, 37.2, 38.1, 35.8, 39.0)
4const maxTemp = Math.max(...temperatures);
5const minTemp = Math.min(...temperatures);
6console.log('Max:', maxTemp); // 39
7console.log('Min:', minTemp); // 35.8Sans les points, Math.max(temperatures) recevrait un seul tableau et renverrait NaN. La console affiche 39, et non 39.0, car les nombres ne conservent pas les zéros finaux. Le spread fonctionne sur toute valeur itérable, donc [...'DNA'] donne ['D', 'N', 'A'].
L'opérateur spread pour les objets
Le spread fonctionne aussi avec les objets et "déballe" les paires clé-valeur dans un nouvel objet :
1const baseDino = { species: 'Unknown', health: 100, zone: 'X' };
2
3// Copier un objet
4const dinoCopy = { ...baseDino };
5console.log(dinoCopy); // { species: 'Unknown', health: 100, zone: 'X' }
6
7// Écraser des propriétés (la dernière gagne !)
8const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
9console.log(rex); // { species: 'T-Rex', health: 100, zone: 'A' }
10
11// Fusionner des objets
12const position = { lat: 12.34, lon: 56.78 };
13const status = { health: 85, alert: false };
14const fullReport = { ...position, ...status, timestamp: Date.now() };
15console.log(fullReport);
16// { lat: 12.34, lon: 56.78, health: 85, alert: false, timestamp: ... }Le spread copie les propriétés propres et énumérables de l'objet, y compris celles dont la clé est un Symbol. baseDino reste un modèle à partir duquel on peut créer d'autres spécimens.
L'ordre compte
Lors de la fusion d'objets, les propriétés placées plus loin écrasent les précédentes. C'est pourquoi il faut toujours déballer les réglages par défaut en premier :
1const defaults = { voltage: 10000, locked: true, zone: 'A' };
2const userSettings = { voltage: 15000, zone: 'B' };
3
4// Les valeurs par défaut d'abord, puis l'écrasement
5const config = { ...defaults, ...userSettings };
6console.log(config);
7// { voltage: 15000, locked: true, zone: 'B' }
8
9// Ordre inversé - les valeurs par défaut écraseraient les réglages !
10const wrong = { ...userSettings, ...defaults };
11console.log(wrong);
12// { voltage: 10000, zone: 'A', locked: true } - faux !La position d'une clé est fixée par sa première apparition, et sa valeur par la dernière. C'est pourquoi, dans wrong, les clés sont dans un autre ordre, et ce sont les valeurs par défaut qui ont gagné. De même, { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } } donne { a: 1, b: 3, c: 4 }.
L'opérateur rest dans les fonctions
Le rest (...) dans les paramètres d'une fonction rassemble tous les arguments restants dans un tableau. Un observateur peut ainsi signaler un nombre quelconque d'espèces :
1// Paramètre rest - rassemble un nombre variable d'arguments
2function logSighting(observer, ...dinosaurs) {
3 console.log(`${observer} a observé : ${dinosaurs.join(', ')}`);
4 console.log(`Nombre d'espèces : ${dinosaurs.length}`);
5}
6
7logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
8// "Dr. Rex a observé : T-Rex, Velociraptor, Triceratops"
9// "Nombre d'espèces : 3"
10
11logSighting('Dr. Rex', 'Brachio');
12// "Dr. Rex a observé : Brachio"
13// "Nombre d'espèces : 1"Le premier argument va dans observer, et tout le reste dans un véritable tableau dinosaurs, sur lequel join et length fonctionnent donc. L'ancien objet arguments n'est pas un tableau, c'est pourquoi je te recommande le rest dans le code moderne.
Le rest doit être le dernier paramètre
Le rest rassemble tout ce qui reste, il doit donc se trouver à la fin de la liste des paramètres. Sinon, JavaScript ne saurait pas où arrêter la collecte :
1// Correct - le rest à la fin
2function feedDinos(keeper, time, ...dinoNames) {
3 dinoNames.forEach(name => {
4 console.log(`${keeper} nourrit ${name} à ${time}`);
5 });
6}
7
8feedDinos('Alan', '08:00', 'Rex', 'Blue', 'Delta');
9
10// Erreur ! Le rest ne peut pas précéder d'autres paramètres
11// function wrong(...names, time) {} // SyntaxError!Une fonction ne peut avoir qu'un seul paramètre rest, et c'est toujours le dernier. Toute autre position se termine par une erreur SyntaxError avant même l'exécution du code.
Le rest dans la déstructuration
Le rest fonctionne aussi dans la déstructuration des tableaux et des objets, que tu connais depuis la leçon précédente :
1// Rest dans la déstructuration de tableau
2const rankings = ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Giganotosaurus'];
3const [champion, runnerUp, ...others] = rankings;
4console.log(champion); // "T-Rex"
5console.log(runnerUp); // "Velociraptor"
6console.log(others); // ["Spinosaurus", "Giganotosaurus"]
7
8// Rest dans la déstructuration d'objet
9const sensor = { id: 'S-001', value: 42, unit: 'C', zone: 'A', active: true };
10const { id: sensorId, value: sensorValue, ...metadata } = sensor;
11console.log(sensorId); // "S-001"
12console.log(sensorValue); // 42
13console.log(metadata); // { unit: 'C', zone: 'A', active: true }Dans les deux cas, le rest crée un nouveau tableau ou un nouvel objet avec ce qui n'a pas été nommé.
Les motifs pratiques
Retirer des propriétés d'un objet
Combiner la déstructuration et le rest permet d'omettre des champs sans modifier l'objet, par exemple pour cacher des données internes avant d'envoyer un rapport :
1const dino = { name: 'Rex', _secret: 'code', health: 100, _internal: true };
2
3// Extrais ce que tu veux retirer, le reste demeure
4const { _secret, _internal, ...publicData } = dino;
5console.log(publicData); // { name: 'Rex', health: 100 }L'objet dino a toujours _secret. Les champs extraits sont devenus des variables séparées, et publicData est un nouvel objet sans eux.
Créer une copie modifiée
Le spread est aussi très utile pour mettre à jour des données. Au lieu de modifier l'original, tu crées une nouvelle version avec des champs écrasés :
1function updateDino(dino, updates) {
2 return { ...dino, ...updates, lastModified: Date.now() };
3}
4
5const rex = { name: 'Rex', health: 100, zone: 'A' };
6const updatedRex = updateDino(rex, { health: 85, zone: 'B' });
7console.log(updatedRex);
8// { name: 'Rex', health: 85, zone: 'B', lastModified: ... }
9console.log(rex.health); // 100 - l'original est inchangé !L'ancien enregistrement reste intact, il est donc facile de comparer l'état avant et après la modification. C'est sur ce motif que repose la mise à jour de l'état dans React et Redux.
Pratique
Dans le bac à sable t'attendent la fusion de tableaux, les copies, Math.max avec le spread, l'ordre lors de la fusion d'objets et le rest dans les fonctions. Inverse l'ordre des spreads et vérifie quelle valeur gagne.
Code de cette leçon : index.js
1// === OPÉRATEURS SPREAD ET REST ===
2
3// 1. Spread - combinaison de tableaux
4const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
5const herbivores = ['Triceratops', 'Brachiosaurus'];
6const allDinos = [...carnivores, ...herbivores];
7console.log('=== Spread tableaux ===');
8console.log('Tous:', allDinos);
9
10// 2. Copie de tableau (copie superficielle)
11const original = ['A', 'B', 'C'];
12const copy = [...original];
13copy.push('D');
14console.log('\n=== Copie ===');
15console.log('Original:', original);
16console.log('Copie:', copy);
17
18// 3. Spread avec des fonctions
19const temps = [36.5, 37.2, 38.1, 35.8, 39.0];
20console.log('\n=== Spread avec des fonctions ===');
21console.log('Température max:', Math.max(...temps));
22console.log('Température min:', Math.min(...temps));
23
24// 4. Spread pour les objets
25const baseDino = { species: 'Inconnue', health: 100 };
26const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
27console.log('\n=== Spread sur les objets ===');
28console.log('Rex:', JSON.stringify(rex));
29
30// Combinaison d'objets
31const position = { lat: 12.34, lon: 56.78 };
32const status = { health: 85, alert: false };
33const fullReport = { ...position, ...status };
34console.log('Rapport:', JSON.stringify(fullReport));
35
36// 5. L'ordre a de l'importance
37const defaults = { voltage: 10000, locked: true };
38const settings = { voltage: 15000 };
39console.log('\n=== Ordre ===');
40console.log('Correct:', JSON.stringify({ ...defaults, ...settings }));
41console.log('Mauvais:', JSON.stringify({ ...settings, ...defaults }));
42
43// 6. Rest dans les fonctions
44function logSighting(observer, ...dinosaurs) {
45 console.log(observer + ' voit: ' + dinosaurs.join(', '));
46 console.log('Nombre: ' + dinosaurs.length);
47}
48console.log('\n=== Rest dans les fonctions ===');
49logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
50
51// 7. Rest dans la déstructuration
52const [champion, runnerUp, ...others] = ['T-Rex', 'Raptor', 'Spino', 'Giga'];
53console.log('\n=== Rest dans la déstructuration ===');
54console.log('Champion:', champion);
55console.log('Reste:', others);
56
57const { name, ...dinoStats } = { name: 'Rex', weight: 8000, zone: 'A' };
58console.log('Nom:', name);
59console.log('Statistiques:', JSON.stringify(dinoStats));Retiens bien : les mêmes trois points, du côté droit, déchargent le conteneur, et dans les paramètres ou à gauche d'une affectation, ils emballent les échantillons dans un nouveau récipient.
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 va retourner [...['A', 'B'], ...['C', 'D']] ?
2. Que va retourner { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } } ?
3. Où le paramètre rest (...args) doit-il se trouver dans la liste des paramètres d'une fonction ?
4. Pourquoi spread crée-t-il une « copie superficielle » des objets ?
Tâches pratiques dans le jeu
- Éditeur de code
Tu as les tableaux const carnivores = ['T-Rex', 'Velociraptor'] et const herbivores = ['Triceratops'] ainsi que l'objet const defaults = { zone: 'A', locked: true }. Avec l'opérateur spread, crée : allDinos (la fusion des deux tableaux), carnivoresCopy (une nouvelle copie de carnivores) et config (defaults écrasé par zone: 'B'). Écris aussi une fonction countSightings(observer, ...dinos) qui renvoie le nombre de dinosaures transmis.
- Tri horizontal
Range les éléments de la création d'une copie de tableau à l'aide de spread :
- Éditeur de code
Tu as un objet const dino = { name: 'Rex', health: 100, _secret: 'code', _internal: true }. Avec la déstructuration et l'opérateur rest, crée un nouvel objet publicData sans les champs _secret et _internal (dino doit rester inchangé). Écris une fonction updateDino(dino, updates) qui renvoie un nouvel objet : une copie de dino écrasée par les champs de updates, sans modifier l'objet transmis.
- Éditeur de code
Crée deux identifiants d'échantillons d'ADN : const sampleId1 = Symbol('DNA-001') et const sampleId2 = Symbol('DNA-001'). Affiche dans la console le résultat de sampleId1 === sampleId2. Ensuite, crée un objet specimen avec la propriété name: 'T-Rex' et les données de l'échantillon (par exemple { purity: 98.5 }) enregistrées sous la clé sampleId1.