La copie superficielle
Copier un objet sans partager ses propriétés imbriquées.
Objectifs
À la fin de cette leçon, vous saurez :
- dupliquer un objet ou un tableau avec la syntaxe d'étalement
..., en comprenant ce qu'elle fabrique ; - expliquer précisément ce que copie une copie « superficielle » — et ce qu'elle partage encore ;
- fusionner des objets sans muter les originaux ;
- connaître les limites du bricolage JSON et l'existence de
structuredClone.
🔗 Pour vous rafraîchir la mémoire : objets et références · le passage de paramètres
Le problème : partager n'est pas copier
Après la leçon sur les références, ce code ne surprend plus personne — mais il reste un problème à résoudre :
const original = { name: "Paul", age: 20 };
const copie = original; // même adresse, pas une copie
copie.age = 21;
console.log(original.age); // 21 : catastrophe
On veut parfois deux objets indépendants : modifier l'un sans ébranler l'autre. Écrire chaque propriété à la main fonctionne mais vieillit mal (oubli d'une propriété, maintenance pénible). Il faut un vrai mécanisme de duplication.
La solution : l'étalement (spread)
const original = { name: "Paul", age: 20 };
const copie = { ...original }; // « étale » les propriétés dans un NOUVEL objet
copie.age = 21;
console.log(original.age); // 20 : intact !
console.log(copie.age); // 21
console.log(copie === original); // false : deux adresses distinctes
Que fait { ...original } exactement ? Trois gestes en un :
- crée un nouvel objet vide (nouvelle adresse en mémoire) ;
- parcourt les propriétés de
originalau premier niveau ; - recopie chaque valeur dans l'objet neuf.
Même syntaxe pour les tableaux, entre crochets :
const nombres = [1, 2, 3];
const clone = [...nombres];
clone.push(4);
console.log(nombres.length); // 3 : intact
console.log(clone.length); // 4
Autres façons équivalentes pour les tableaux, à savoir reconnaître dans le code des autres : nombres.slice() (sans argument) et Array.from(nombres).
Vérification du contrat par le test d'identité vu hier :
console.log([...nombres] === nombres); // false : nouveau tableau
console.log({ ...original } === original);// false : nouvel objet
Pourquoi « superficielle » : le niveau profond reste partagé
L'étalement recopie les valeurs du premier niveau. Mais rappelez-vous : si une propriété contient un objet, sa « valeur » est... une référence. Et une référence se copie telle quelle — donc l'objet imbriqué reste partagé :
const original = {
name: "Paul",
adresse: { ville: "Lyon" }, // propriété de second niveau : un objet
};
const copie = { ...original };
copie.name = "Jean"; // niveau 1 : indépendant ✓
copie.adresse.ville = "Paris"; // niveau 2 : mutation de L'OBJET PARTAGÉ ✗
console.log(original.name); // "Paul" : protégé
console.log(original.adresse.ville); // "Paris" : touché quand même !!
console.log(copie.adresse === original.adresse); // true : même adresse
Schéma mémoire après la copie :
original ──→ { name: "Paul", adresse ──┐
├──→ { ville: "Paris" } UN SEUL objet interne
copie ────→ { name: "Jean", adresse ──┘
↑ nouveau ↑ référence recopiée
objet → profondeur NON dupliquée
D'où le nom : la copie est superficielle — elle s'arrête à la surface. Analogie : photocopier la page de garde d'un dossier ; les annexes restent partagées entre les deux classeurs.
Le piège existe aussi côté tableaux imbriqués :
const config = { tags: ["js", "web"] };
const clone = { ...config };
clone.tags.push("mémoire");
console.log(config.tags); // [ 'js', 'web', 'mémoire' ] : partagé !
Règle de vigilance : avant de muter une copie superficielle, demandez-vous « mes données contiennent-elles des objets ou tableaux ? ». Si oui, ces membres-là restent communs.
Copier en profondeur : deux outils
Pour dupliquer tous les niveaux, deux approches :
// 1. Le classique JSON : sérialiser puis désérialiser
const copieProfonde = JSON.parse(JSON.stringify(original));
// 2. L'outil natif moderne (Node ≥ 17)
const autreCopie = structuredClone(original);
structuredClone est le bon choix aujourd'hui : il gère les dates, les tableaux imbriqués, les Map... La variante JSON fonctionne seulement pour des données « simples » (textes, nombres, booléens, tableaux, objets) et perd silencieusement tout le reste (undefined, fonctions, dates transformées en textes). Vérifiez :
const data = { quand: new Date("2026-01-12") };
const viaJson = JSON.parse(JSON.stringify(data));
console.log(viaJson.quand instanceof Date); // false : c'est devenu du texte !
Pour ce cursus : retenez que la copie profonde existe, qu'elle coûte plus cher, et que la plupart des cas réels se règlent avec une copie superficielle bien comprise.
L'étalement a un deuxième talent : fusionner
Placer plusieurs étalements dans un même objet fusionne leurs contenus, les derniers gagnant en cas de conflit :
const defauts = { theme: "clair", langue: "fr", notifications: true };
const options = { theme: "sombre" }; // l'utilisateur ne choisit que ça
const config = { ...defauts, ...options };
console.log(config);
// { theme: 'sombre', langue: 'fr', notifications: true }
// ↑ écrasé par options ↑ repris de defauts
C'est le motif universel « valeurs par défaut + préférences utilisateur », et il est non destructif : ni defauts ni options ne sont modifiés — un troisième objet naît. Vous utiliserez ce motif dans presque chaque projet.
Exercice
- Créez
livre = { titre: "Cours", pages: 120 }. Copiez-le avec l'étalement, passez la copie à 150 pages, vérifiez que l'original reste à 120.
const a = { tags: ["js", "web"] };
const b = { ...a };
b.tags.push("mémoire");
console.log(a.tags);
Prédisez puis expliquez avec les mots « référence » et « premier niveau ».
-
Écrivez
fusionner(defauts, options)qui retourne un nouvel objet combinant les deux (options gagnantes), sans modifier aucun des deux. Testez avec les objets de la section précédente. -
Écrivez
retirerDernier(liste)qui retourne un nouveau tableau privé du dernier élément, sans toucher à l'original. Deux implémentations acceptées — écrivez les deux si vous y arrivez. -
Réparez le problème de l'exercice 2 : produisez dans
bun clone deadont le tableautagssoit aussi indépendant (indice : double étalement). -
Une phrase : pourquoi dit-on « superficielle » ?
Résumé
{ ...obj }/[ ...tab ]: nouvel objet, contenu du premier niveau recopié.- Les propriétés-objets sont recopiées par référence : niveaux profonds toujours partagés.
{ ...defauts, ...options }fusionne sans détruire — les derniers étalements gagnent.- Copie profonde :
structuredClone(propre), JSON round-trip (limitée). - Avant de muter une copie : vérifiez la profondeur de vos données.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Exercice 1.
const livre = { titre: "Cours", pages: 120 };
const exemplaire = { ...livre };
exemplaire.pages = 150;
console.log(livre.pages); // 120
console.log(exemplaire.pages); // 150
console.log(livre === exemplaire); // false
Propriétés primitives au premier niveau → copiées par valeur → indépendance totale.
Exercice 2. Affiche [ 'js', 'web', 'mémoire' ]. Explication attendue : « l'étalement a créé un nouvel objet et recopié son premier niveau — mais la propriété tags contient une référence vers un tableau ; cette référence a été recopiée telle quelle. Les deux objets pointent donc le MÊME tableau interne, et le push (mutation) est visible des deux côtés. »
Exercice 3.
function fusionner(defauts, options) {
return { ...defauts, ...options };
}
const defauts = { theme: "clair", langue: "fr" };
const options = { theme: "sombre" };
const config = fusionner(defauts, options);
console.log(config); // { theme: 'sombre', langue: 'fr' }
console.log(defauts.theme); // "clair" : intact
console.log(options.langue); // undefined : jamais touché non plus
L'ordre des étalements porte le sens : options écrit après, donc ses valeurs écrasent celles de defauts en cas de doublon.
Exercice 4.
// Version A : slice négatif (« jusqu'à l'index -1 exclu » = tout sauf le dernier)
function retirerDernier(liste) {
return liste.slice(0, -1);
}
// Version B : copie puis retrait local
function retirerDernierBis(liste) {
const copie = [...liste];
copie.pop(); // mutation... de MA copie : parfaitement sain
return copie;
}
const source = ["a", "b", "c"];
console.log(retirerDernier(source)); // [ 'a', 'b' ]
console.log(source); // [ 'a', 'b', 'c' ] intact
Remarquez la version B : muter une copie locale est parfaitement légitime — la discipline « ne pas muter » concerne les données des autres, pas les vôtres.
Exercice 5.
const a = { tags: ["js", "web"] };
const b = { ...a, tags: [...a.tags] }; // niveau 1 recopié ET tableau interne cloné
b.tags.push("mémoire");
console.log(a.tags); // [ 'js', 'web' ] : protégé enfin !
console.log(b.tags); // [ 'js', 'web', 'mémoire' ]