22 octobre 2021

Les références d'objet et leur copie

Une des différences fondamentale des objets avec les primitives est que ceux-ci sont stockés et copiés “par référence”, en opposition des valeurs primitives : strings, numbers, booleans, etc. – qui sont toujours copiés comme “valeur entière”.

On comprendra plus facilement en regardant “sous le capot” de ce qui se passe lorsque nous copions une valeure.

Commençons avec une primitive, comme une chaîne de caractères.

Ici nous assignons une copie de message dans phrase :

let message = "Hello!";
let phrase = message;

Il en résulte deux variables indépendantes, chacune stockant la chaîne de caractères "Hello!".

Un résultat plutôt évident n’est-ce pas ?

Les objets ne fonctionnent pas comme cela.

Une variable assignée à un objet ne stocke pas l’objet lui-même, mais son “adresse en mémoire”, en d’autres termes “une référence” à celui-ci.

Prenons un exemple d’une telle variable :

let user = {
  name: "John"
};

Et ici comment elle est stockée en mémoire :

L’objet est stocké quelque part dans la mémoire (du coté droit de l’image), tandis que la varaible user (du coté gauche) a une référence à celui-ci.

On peut imaginer la variable d’objet, ici user, comme une feuille de papier avec l’adresse de l’objet écrit dessus.

Lorque l’on réalise une action avec l’objet, par exemple récupérer la propriété user.name, le moteur de Javascript regarde à l’adresse et réalise l’opération sur l’objet actuel.

Et voilà pourquoi cela est important.

Lorsqu’une variable d’objet est copiée – la référence est copiée, l’objet lui-même n’est pas dupliqué.

Par exemple:

let user = { name: "John" };

let admin = user; // copie la référence

Maintenant nous avons deux variables, chacune avec la référence vers le même objet :

Comme vous pouvez le voir, il n’y a toujours qu’un seul objet, mais maintenant avec deux variables qui le référence.

On peut utiliser n’importe quelle variable pour accéder à l’objet et modifier son contenu :

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // changé par la référence "admin"

alert(user.name); // 'Pete', les changements sont visibles sur la référence "user"

C’est comme si nous avions une armoire avec deux clés et que nous en utilisions une (admin) pour y entrer et y apporter des modifications. Ensuite, si nous utilisons plus tard une autre clé (user), nous ouvrons toujours la même armoire et pouvons accéder au contenu modifié.

Comparaison par référence

Deux objets sont égaux seulement s’ils sont le même objet.

Par exemple, ici a et b référencent le même objet, aussi sont-ils similaires :

let a = {};
let b = a; // copie la référence

alert( a == b ); // true, les deux variables référencent le même objet
alert( a === b ); // true

Et ici deux objets indépendants ne sont pas égaux, même s’ils se ressemblent (les deux sont vides) :

let a = {};
let b = {}; // 2 objets indépendants

alert( a == b ); // false

Pour des comparaisons comme obj1 > obj2 ou des comparaisons avec une primitive obj == 5, les objets sont convertis en primitives. Nous étudierons comment les conversions d’objets fonctionnent très bientôt, mais pour dire la vérité, de telles comparaisons sont rarement nécessaires, en général elles sont le résultat d’une erreur de programmation.

Clonage et fusion, Object.assign

Copier une variable object créé une référence en plus vers le même objet.

Mais quid si nous voulons dupliquer un objet ? Créer une copie indépendante, un clone ?

C’est aussi faisable, mais un peu plus compliqué, parce qu’en Javascript il n’y pas de méthode intégrée pour cela. En fait c’est rarement utile. Copier par référence fonctionne la plupart du temps.

Mais si nous le voulons, alors nous devons créer un nouvel objet et répliquer sa structure en itérant ses propriétés et en les copiant au niveau primitive.

Comme cela :

let user = {
  name: "John",
  age: 30
};

let clone = {}; // le nouvel object vide

// on copie toutes les propritété de user
for (let key in user) {
  clone[key] = user[key];
}

// maintenant clone est un objet complétemnet indépendant  avec le même contenu
clone.name = "Pete"; // On change les données de celui-ci

alert( user.name ); // c'est toujour john dans l'objet copié

On peut aussi utiliser la méthode Object.assign pour cela.

La syntaxe est :

Object.assign(dest, [src1, src2, src3...])
  • Le premier argument dest est l’objet cible
  • Les arguments suivants src1, ..., srcN (cela peut-être tant que l’on veut) sont les objets à copier.
  • La méthode copie les propriétés de tous les objets à copier src1, ..., srcN dans l’objet dest. En d’autres mots, les propriétés de tous les arguments à partir du deuxième sont copiés dans le premier argument.
  • L’appel retourne dest.

Par exemple, on peut l’utiliser pour fusioner plusieurs objets en un seul :

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// copie toutes les propriétés de permissions1 et 2 dans user
Object.assign(user, permissions1, permissions2);

// on a user = { name: "John", canView: true, canEdit: true }

Si la propriété copiée existe déja, elle est écrasée.

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // on a user = { name: "Pete" }

On peut aussi utiliser Object.assign pour remplacer la boucle for..in pour un clonage simple.

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

Cela copie toutes les propriétés de user dans l’objet vide et le retourne.

Il existe également d’autres méthodes de clonage d’un objet, par ex. en utilisant la syntaxe spread clone = {...user}, abordé plus loin dans le tutoriel.

Clonage imbriqué

Jusqu’à maintenant on suppose que toutes les propriétés de use sont des primitives. Mais les propriétés peuvent être des références vers d’autres objets. Comment gèrer ces cas-là ?

Comme ceci :

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

Ce n’est plus suffisant de copier clone.sizes = user.sizes, car user.sizes est un objet, il sera copié par référence. Donc clone et user partageront le même objet sizes :

Comme cela :

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, c'est le même objet

// user et clone partage l'objet sizes
user.sizes.width++;       // on modifie la propriété à un endroit
alert(clone.sizes.width); // 51, on peut voir la modification dans un autre endroit

Pour régler ça, on doit utiliser la boucle de clonage qui examine chaque valeur de user[key] et, si c’est un objet, répliquer sa structure aussi. On appelle cela un “clone réel” (deep clone).

On peut utiliser la récursion pour l’implémenter. Ou, pour ne pas réinventer la roue, prendre un implémentation existante. par exemple _.cloneDeep(obj) de la librairie lodash.

Les objets Const peuvent être modifiés

Un “effet secondaire” important du stockage d’objets en tant que références est qu’un objet déclaré comme const peut être modifié.

Par exemple :

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

Il peut sembler que la ligne (*) provoquerait une erreur, mais non. La valeur de user est constante, elle doit toujours référencer le même objet. Mais les propriétés de cet objet sont libres de changer.

En d’autres termes, le const user ne donne une erreur que si nous essayons de définir user = ... dans son ensemble, et c’est tout.

Cela dit, si nous avons vraiment besoin de créer des propriétés d’objet constantes, c’est également possible, mais en utilisant des méthodes totalement différentes, nous le mentionnerons dans le chapitre Attributs et descripteurs de propriétés.

Résumé

Les objets sont assignés et copiés par référence. En d’autres termes, une variable ne stocke pas la “valeur de l’objet” mais la “référence” (l’adresse en mémoire) de la valeur. Donc copier cette variable, ou la passer en argument d’une fonction, copie la référence, pas l’objet lui-même.

Toutes les opérations faites par une copie de la référence (comme ajouter/supprimer une propriété) sont faites sur le même objet.

Pour réaliser une copie (un clone) on peut utiliser Object.assign, pour faire une “copie superficielle” (les objets imbriqués sont copiés par référence), ou pour une “copie réelle” une fonction comme _.cloneDeep(obj).

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)