Vous tracez un bug à travers trois fichiers pour finalement découvrir qu'une fonction utilitaire inoffensive a renommé un utilisateur. Vous n'avez jamais touché à l'original. Ou du moins, c'est ce que vous pensiez. En JavaScript, assigner une variable à une autre ne signifie pas toujours ce que vous attendez. Le langage divise ses données en deux stratégies de stockage, et oublier laquelle vous utilisez est la raison pour laquelle des mutations silencieuses s'immiscent dans le code de production.

Pour éviter cela, vous devez percevoir la différence entre les valeurs primitives et les objets exactement comme le moteur le fait.

Primitives : de véritables copies

Une primitive est une donnée unique et indivisible. Elle ne peut pas être décomposée en valeurs JavaScript plus petites. Le langage définit sept types primitifs : String, Number, Boolean, Undefined, Null, Symbol et BigInt.

Comme les primitives sont atomiques, elles résident généralement directement à l'intérieur de la liaison de la variable. Lorsque vous copiez une variable primitive vers une autre, le moteur duplique les données réelles. Chaque variable reçoit son propre emplacement indépendant en mémoire.

let a = "Alina";
let b = a;
b = "Ali";

console.log(a); // "Alina"
console.log(b); // "Ali"

Ici, a reste intact. La réassignation de b a créé une toute nouvelle valeur et a pointé b vers elle, tandis que a conserve toujours sa chaîne de caractères d'origine. Ce comportement est une copie par valeur. Il fonctionne de la même manière pour les nombres, les booléens, les symboles et le reste de la famille des primitives. Vous pouvez les passer à des fonctions, les réassigner ou les retourner sans vous soucier des effets de bord sur les variables sœurs.

Objets : adresses partagées

Les objets sont différents. Un objet est un conteneur composite qui regroupe plusieurs morceaux de données. Cette catégorie inclut les objets simples, les tableaux, les fonctions, les dates et tout autre type non primitif. Comme ces structures peuvent devenir volumineuses et imbriquées, JavaScript ne stocke pas l'objet entier à l'intérieur de la variable. Au lieu de cela, la variable contient une référence, essentiellement une adresse mémoire pointant vers les données réelles stockées ailleurs.

Lorsque vous assignez un objet à une nouvelle variable, le moteur copie l'adresse, pas l'objet. Deux variables pointent désormais vers la même maison.

const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";

console.log(user.name); // "Ali"

Modifier copy.name a également modifié user.name car les deux noms se résolvent vers le même objet sous-jacent. C'est une copie par référence. La même surprise apparaît avec les tableaux :

const scores = [82, 91, 74];
const backup = scores;
backup.push(88);

console.log(scores); // [82, 91, 74, 88]

Il n'y a qu'un seul tableau en mémoire. scores et backup ne sont que deux panneaux pointant vers lui.

Le mot-clé const ajoute une couche de confusion. Déclarer un objet avec const verrouille la liaison de la variable afin que vous ne puissiez pas la pointer vers une nouvelle adresse. Cela ne verrouille pas l'objet lui-même.

const settings = { theme: "dark" };
settings.theme = "light";        // Works perfectly.
settings = { theme: "dark" };    // TypeError

Les développeurs s'attendent souvent à ce que const garantisse l'immuabilité. Ce n'est pas le cas. Il empêche seulement la réassignation de la référence. Si vous voulez que le contenu soit scellé, vous devez copier avec intention.

Là où se cachent les vrais bugs

Les bugs de référence apparaissent rarement sous la forme de réassignations de variables évidentes. Ils se cachent à l'intérieur des appels de fonctions.

function addTimestamp(record) {
  record.timestamp = Date.now();
  return record;
}

const original = { id: 1 };
addTimestamp(original);

console.log(original.timestamp); // A number now exists here. Oops.

Le paramètre record a reçu une copie de la référence. Toute mutation de propriété à l'intérieur de la fonction a été écrite directement dans l'objet de l'appelant. La fonction ressemblait à une simple transformation, et pourtant, elle a laissé fuiter l'état à travers les limites de la portée.

Ce modèle est particulièrement douloureux dans les frameworks d'interface utilisateur comme React, où les mises à jour d'état dépendent