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â 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 variable 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.
Un effet secondaire important du stockage des 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 (*) causerait une erreur, mais ce nâest pas le cas. 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, const user donne une erreur uniquement si nous essayons de définir user=... dans son ensemble.
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.
Clonage et fusion, Object.assign
Copier une variable object crée une référence en plus vers le même objet.
Mais que se passe-t-il si nous devons dupliquer un objet ?
Nous pouvons créer un nouvel objet et reproduire la structure de lâexistant, en itérant sur ses propriétés et en les copiant au niveau primitif.
Comme cela :
let user = {
name: "John",
age: 30
};
let clone = {}; // le nouvel object vide
// on copie toutes les propritétés de user
for (let key in user) {
clone[key] = user[key];
}
// maintenant clone est un objet complètement 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, ...sources)
- Le premier argument
destest lâobjet cible - Les autres arguments sont une liste dâobjets source.
Il copie les propriétés de tous les objets sources dans la cible dest, puis les renvoie comme résultat.
Par exemple, nous avons lâobjet user, ajoutons-lui quelques autorisations :
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);
// now user = { name: "John", canView: true, canEdit: true }
alert(user.name); // John
alert(user.canView); // true
alert(user.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" }
Nous pouvons également utiliser Object.assign pour effectuer un simple clonage dâobjet :
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
alert(clone.name); // John
alert(clone.age); // 30
Ici 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 user 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 :
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 = 60; // changer une propriété d'un endroit
alert(clone.sizes.width); // 60, obtenir le résultat de l'autre
Pour résoudre ce problème et faire en sorte que user et clone soient des objets véritablement séparés, nous devrions utiliser une boucle de clonage qui examine chaque valeur de user[key] et, sâil sâagit dâun objet, répliquer également sa structure. Câest ce quâon appelle un « clonage profond » ou « clonage structuré ». Il existe une méthode structuredClone qui implémente le clonage en profondeur.
structuredClone
Lâappel structuredClone(object) clone lâobject avec toutes les propriétés imbriquées.
Voici comment nous pouvons lâutiliser dans notre exemple :
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = structuredClone(user);
alert( user.sizes === clone.sizes ); // false, c'est un objet différent
// user et clone n'ont plus aucun lien entre eux
user.sizes.width = 60; // changer une propriété d'un endroit
alert(clone.sizes.width); // 50, sans lien
La méthode structuredClone peut cloner la plupart des types de données, tels que des objets, des tableaux, des valeurs primitives.
Il prend également en charge les références circulaires, lorsquâune propriété dâobjet fait référence à lâobjet lui-même (directement ou via une chaîne ou des références).
Par exemple :
let user = {};
// créons une référence circulaire :
// user.me fait référence à l'utilisateur lui-même
user.me = user;
let clone = structuredClone(user);
alert(clone.me === clone); // true
Comme vous pouvez le voir, clone.me fait référence au clone, pas à user ! Ainsi, la référence circulaire a également été clonée correctement.
Cependant, il existe des cas où structuredClone échoue.
Par exemple, lorsquâun objet a une propriété de fonction :
// error
structuredClone({
f: function() {}
});
Les propriétés de fonction ne sont pas prises en charge.
Pour gérer des cas aussi complexes, nous devrons peut-être utiliser une combinaison de méthodes de clonage, écrire du code personnalisé ou, pour ne pas réinventer la roue, prendre une implémentation existante, par exemple _.cloneDeep(obj) de la bibliothèque JavaScript lodash.
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 faire une âcopie réelleâ (un clone), nous pouvons utiliser Object.assign pour la soi-disant âcopie superficielleâ (les objets imbriqués sont copiés par référence) ou une fonction de âclonage en profondeurâ structuredClone ou utiliser une implementation personnalisée de clonage, telle que _.cloneDeep(obj).
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)