Apprenez en lisant dans l'ordre

Références et copie — les pièges des tableaux et objets

Assigner un tableau passe une référence : modifier la copie change l'original. Copie superficielle et profonde.

Essaie de sauvegarder une commande avant de la modifier en écrivant const backup = order;, et corriger order plus tard change backup du même coup. Ça n'arrive jamais avec des nombres ou des chaînes.

Cet article couvre ce qui se passe réellement quand tu assignes un tableau ou un objet, et comment construire une copie sûre.

Ce que l'affectation passe — valeurs primitives vs références

Ce qui rend ce décalage difficile à traquer, c'est que ce qui se fait passer à l'affectation diffère selon le genre de valeur. Voici un point sur ce qui se recopie tel quel à l'affectation, et ce qui ne se recopie pas.

Les 5 genres — nombres, chaînes, booléens, null, et undefined (appelés primitives — des valeurs qui ne sont ni des objets ni des tableaux) — voient la valeur elle-même copiée quand tu les assignes.

Les 2 variables ne sont plus liées après cette copie, donc changer l'une ne change jamais l'autre.

Les tableaux et objets sont des types référence (des types qui gardent une information pointant vers où vit une valeur, plutôt que la valeur elle-même). L'affectation ne fait passer que cette information de localisation, donc 2 variables finissent par pointer sur exactement le même tableau ou objet.

Réécris-le à travers l'un ou l'autre nom, et tu réécris la même chose unique dans les deux cas.

L'affectation passe-t-elle une valeur ou un emplacement ?
Assigne unnombre ou une chaîneLa valeur elle-mêmese fait copierGardées comme 2valeurs séparéesUn tableauou objetSeule l'info delocalisation passe2 variables pointentsur 1 choseRéécrit viaun nomPointe sur lamême chose uniqueL'autre changeaussi
Une primitive voit sa valeur copiée et devient indépendante, mais un tableau ou objet a 2 variables qui pointent sur la même chose unique.
// Une primitive voit sa valeur copiée
let price = 3980;
let salePrice = price;
salePrice = 2980;
console.log(price);          // 3980 (non affecté)
console.log(salePrice);      // 2980

// Un tableau pointe sur la même chose unique
const cart = ["Cahier A4"];
const savedCart = cart;
savedCart.push("Marqueur permanent");
console.log(cart.length);    // 2 (cart a grandi aussi)
console.log(cart === savedCart);  // true (ils pointent sur la même chose)

Même avec const, le contenu peut être réécrit

Ce que const fixe, c'est seulement où pointe la variable. Écris const cart = [];, puis cart.push("Marqueur permanent"), et le contenu grandit ; const order = {}, puis order.total = 0, fonctionne aussi.

Ce qui est interdit, c'est seulement la réaffectation qui la fait pointer ailleurs entièrement, comme cart = [...].

Réécris ce que tu pensais être une sauvegarde d'une commande, et vois ce qui se passe. order et backup sont déjà déclarés, avec backup assigné directement à order.

① Extrais le total dans une variable séparée (utilise let, puisque tu vas la réaffecter), réaffecte juste cette variable à 500, et affiche cette variable suivie du total d'order.

② Réécris le total de backup à 0, et affiche le total de backup.

③ Affiche le total de l'order original.

④ Affiche si order et backup pointent sur la même chose.

(Si tout s'exécute correctement, une explication apparaîtra.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Construire une copie — ce que === regarde vraiment

Comme tu viens de le voir, l'affectation seule ne donne pas une copie sûre, séparée de l'original. Tu veux une commande séparée avec juste le montant changé, en laissant l'original intact ? Construis un nouvel objet disposant le contenu avec la syntaxe spread, comme dans { ...order }. Ce qui vaut la peine d'être confirmé, c'est la relation entre ce que tu construis et l'original.

const copiedOrder = { ...order }; construit un objet séparé, donc order === copiedOrder vaut false. Pour les tableaux et objets, === vérifie non pas si le contenu correspond, mais s'ils sont exactement la même chose unique.

Pour comparer le contenu, écris toi-même les choses spécifiques que tu veux comparer — le nombre de clés, des valeurs individuelles, etc.

=== ne regarde pas le contenu
backup = orderPointent sur lamême chose unique=== est true{ ...order }Construit unechose séparée=== est falseLe contenuest identique=== ne regardepas le contenuToujours false
C'est true quand ils pointent sur la même chose, et false pour des choses construites séparément, même avec un contenu identique.
const cart = ["Cahier A4"];
const copiedCart = [...cart];

// Choses séparées, donc === vaut false
console.log(cart === copiedCart);        // false

// Même avec un contenu identique, toujours false
console.log(["A"] === ["A"]);            // false

// Compare le contenu lui-même, et c'est true
console.log(cart[0] === copiedCart[0]);  // true

// True seulement quand ils pointent sur la même chose
const savedCart = cart;
console.log(cart === savedCart);         // true

Construis une commande séparée avec juste le montant changé, en laissant l'original intact. order est déjà déclaré.

① Construis un objet séparé copiant le contenu d'order, réécris son total à 0, et affiche-le.

② Affiche le total de l'order original.

③ Affiche si order et l'objet de ① sont la même chose.

④ Affiche si les deux ont le même nombre de clés.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

La copie n'atteint pas l'intérieur d'une valeur imbriquée — copie superficielle vs profonde

La syntaxe spread, jusqu'ici, a fait des copies sûres pour des valeurs primitives comme un montant. Mais copie une commande avec un objet imbriqué, comme les infos client, et tandis que l'objet extérieur devient un objet séparé, celui de l'intérieur pointe toujours sur exactement la même chose que l'original. Corrige une adresse sur ce que tu pensais être la copie, et l'adresse de la commande originale change aussi — un décalage auquel tu ne t'attendais pas.

Ce que construit la syntaxe spread est une copie superficielle (une copie qui ne duplique que le premier niveau de propriétés, laissant tout objet imbriqué pointer sur la même chose). Les valeurs du premier niveau deviennent indépendantes si ce sont des primitives, mais quand ce qui est à l'intérieur est un objet ou un tableau, l'information de localisation — la référence — se fait juste copier telle quelle.

Quand tu as besoin que l'intérieur soit reconstruit aussi, utilise une copie profonde (une copie qui reconstruit tout, jusque dans les valeurs imbriquées). Écris structuredClone(order), et chaque niveau d'imbrication se fait reconstruire comme un objet séparé.

C'est une fonction que le navigateur fournit, donc ça marche directement dans la console de ce site aussi. Ça ne marche juste pas sur un objet qui contient une fonction.

Jusqu'où une copie atteint réellement
{ ...order }Reconstruit justele 1er niveauorderId devientindépendantLe customerimbriquéLa référence sefait juste copierChange-le, etl'original changestructuredCloneReconstruit jusqu'aufondChange-le, etl'original est intact
La syntaxe spread ne reconstruit que le premier niveau. Utilise structuredClone quand tu as besoin que l'intérieur soit indépendant aussi.
const order = {
  orderId: "ORD-1342",
  customer: { name: "Kenji Ross", city: "Lyon" },
};

// Copie superficielle : le 1er niveau est séparé, l'intérieur pointe toujours sur la même chose
const shallow = { ...order };
shallow.orderId = "ORD-9999";
console.log(order.orderId);                      // ORD-1342 (indépendant)

shallow.customer.city = "Nantes";
console.log(order.customer.city);                // Nantes (l'original a changé aussi)
console.log(order.customer === shallow.customer);  // true

// Copie profonde : même l'intérieur devient séparé
const deep = structuredClone(order);
deep.customer.city = "Rennes";
console.log(order.customer.city);                // Nantes (l'original est intact)
console.log(order.customer === deep.customer);   // false

Copie une commande qui a des infos client imbriquées à l'intérieur, et vois jusqu'où une réécriture atteint. order est déjà déclaré.

① Construis une copie avec la syntaxe spread, réécris la ville de la copie en « Marseille », puis affiche la ville de l'order original.

② Affiche si le customer de l'original et de la copie sont la même chose.

③ Construis une copie séparée qui reconstruit jusqu'au fond, réécris sa ville en « Toulouse », puis affiche la ville de l'order original.

④ Affiche si le customer de l'original et de la copie de ③ sont la même chose.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Empêcher les réécritures — Object.freeze

Comme tu l'as vu, les tableaux et objets peuvent finir modifiés sans le vouloir, juste en partageant une référence. Certaines valeurs, comme des paramètres par défaut ou un taux de taxe, demandent une protection différente de const — tu ne veux pas qu'elles soient réécrites plus tard. const seul n'empêche pas le contenu d'être réécrit, donc tu as besoin d'un moyen de marquer l'objet lui-même comme interdit aux changements.

Écris Object.freeze(settings), et cet objet arrête d'accepter des changements, ajouts, ou retraits sur ses propriétés. Essaie d'assigner, ajouter, ou supprimer quelque chose après l'avoir gelé, et ça lève un TypeError directement là, arrêtant l'exécution.

Vérifie si quelque chose est gelé avec Object.isFrozen(settings). Geler n'atteint que le premier niveau, donc un objet imbriqué doit être gelé séparément.

Une réécriture après le gel s'arrête avec un TypeError
Assignation avantle gelAccepténormalementDevient lanouvelle valeurAssignation aprèsle gelLève unTypeErrorL'exécutions'arrête làAjout/retraitaprès le gelLève unTypeErrorL'exécutions'arrête là
Une fois gelé, assignation, ajout, et retrait échouent tous. Un TypeError se déclenche directement là, et rien après cette ligne ne s'exécute.
const settings = { theme: "dark" };

// Réécrivable avant le gel
settings.theme = "light";
console.log(settings.theme);             // light

Object.freeze(settings);
console.log(Object.isFrozen(settings));  // true

// Assigner après le gel s'arrête sur cette ligne
// settings.theme = "dark";
// TypeError: Cannot assign to read only property 'theme' of object '#<Object>'

// Le gel n'atteint que le 1er niveau — l'intérieur ne s'arrête pas
const nested = Object.freeze({ display: { theme: "dark" } });
nested.display.theme = "light";
console.log(nested.display.theme);       // light

// Pour le changer, construis un nouvel objet à la place
const updated = { ...settings, theme: "dark" };
console.log(updated.theme);              // dark
console.log(settings.theme);             // light (l'original est intact)

Pour le changer, construis un nouvel objet

Assigner, ajouter, ou supprimer sur un objet gelé lève un TypeError, et rien après cette ligne ne s'exécute. Pour changer un objet gelé, ne le réécris pas — construis-en un nouveau à la place, comme dans { ...settings, theme: "light" }.

Le code ci-dessous essaie de réécrire des paramètres de taux gelés après coup. Exécute-le tel quel et vérifie jusqu'où ça va et quel message l'arrête.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Écris const backup = order;, puis exécute backup.total = 0;. Que se passe-t-il pour order.total ?

Question 2Avec const a = ["A"]; const b = ["A"];, que renvoie a === b ?

Question 3Copie des données avec un objet imbriqué en utilisant { ...order }. Que se passe-t-il pour l'objet imbriqué ?