L'une des différences fondamentales entre les objets et les primitives est que les objets sont stockés et copiés « par référence », alors que les valeurs primitives : chaînes, nombres, booléens, etc. – sont toujours copiées « dans leur ensemble ».
C'est facile à comprendre si l'on regarde un peu ce qui se passe lorsque nous copions une valeur.
Commençons par une primitive, telle qu'une chaîne.
Ici, nous mettons une copie du message
en phrase
:
let message = "Bonjour !"; soit phrase = message ;
En conséquence, nous avons deux variables indépendantes, chacune stockant la chaîne "Hello!"
.
Un résultat assez évident, non ?
Les objets ne sont pas comme ça.
Une variable affecté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.
Regardons un exemple d'une telle variable :
laissez l'utilisateur = { prénom : "Jean" } ;
Et voici comment il est réellement stocké en mémoire :
L'objet est stocké quelque part en mémoire (à droite de l'image), tandis que la variable user
(à gauche) y a une « référence ».
Nous pouvons penser à une variable d'objet, telle que user
, comme une feuille de papier sur laquelle figure l'adresse de l'objet.
Lorsque nous effectuons des actions avec l'objet, par exemple en prenant une propriété user.name
, le moteur JavaScript examine ce qui se trouve à cette adresse et effectue l'opération sur l'objet réel.
Voici maintenant pourquoi c'est important.
Lorsqu'une variable objet est copiée, la référence est copiée, mais l'objet lui-même n'est pas dupliqué.
Par exemple:
let user = { nom : "John" } ; laissez admin = utilisateur ; // copie la référence
Nous avons maintenant deux variables, chacune stockant une référence au même objet :
Comme vous pouvez le voir, il y a toujours un objet, mais maintenant avec deux variables qui le référencent.
Nous pouvons utiliser l'une ou l'autre variable pour accéder à l'objet et modifier son contenu :
let user = { nom : 'John' } ; laissez admin = utilisateur ; admin.name = 'Pete'; // modifié par la référence "admin" alert(utilisateur.nom); // 'Pete', les changements sont visibles à partir de la référence "user"
C'est comme si nous avions une armoire avec deux clés et que nous utilisions l'une d'elles ( admin
) pour y accéder et apporter des modifications. Ensuite, si nous utilisons ultérieurement une autre clé ( user
), nous ouvrons toujours la même armoire et pouvons accéder au contenu modifié.
Deux objets ne sont égaux que s’il s’agit du même objet.
Par exemple, ici a
et b
font référence au même objet, ils sont donc égaux :
soit a = {} ; soit b = a; // copie la référence alerte( a == b ); // vrai, les deux variables font référence au même objet alerte( a === b ); // vrai
Et ici deux objets indépendants ne sont pas égaux, même s’ils se ressemblent (les deux sont vides) :
soit a = {} ; soit b = {} ; // deux objets indépendants alerte( a == b ); // FAUX
Pour des comparaisons comme obj1 > obj2
ou pour une comparaison avec une primitive obj == 5
, les objets sont convertis en primitives. Nous étudierons très prochainement le fonctionnement des conversions d'objets, mais à vrai dire, de telles comparaisons sont très rarement nécessaires – elles apparaissent généralement à la suite d'une erreur de programmation.
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:
utilisateur const = { prénom : "Jean" } ; nom d'utilisateur = "Pete" ; // (*) alert(utilisateur.nom); // Pierre
Il pourrait sembler que la ligne (*)
provoque une erreur, mais ce n'est pas le cas. La valeur de user
est constante, elle doit toujours faire référence au même objet, mais les propriétés de cet objet sont libres de changer.
En d'autres termes, l' 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 Indicateurs et descripteurs de propriétés.
Ainsi, copier une variable objet crée une référence supplémentaire au même objet.
Mais que se passe-t-il si nous devons dupliquer un objet ?
Nous pouvons créer un nouvel objet et répliquer la structure de l'existant, en parcourant ses propriétés et en les copiant au niveau primitif.
Comme ça:
laissez l'utilisateur = { nom : "Jean", âge : 30 ans } ; laissez clone = {} ; // le nouvel objet vide // copions toutes les propriétés utilisateur dedans pour (laisser la clé dans l'utilisateur) { clone[clé] = utilisateur[clé]; } // maintenant le clone est un objet totalement indépendant avec le même contenu clone.name = "Pete" ; // a modifié les données qu'il contient alert( utilisateur.nom ); // toujours John dans l'objet original
On peut également utiliser la méthode Object.assign.
La syntaxe est :
Objet.assign(dest, ...sources)
Le premier argument dest
est un objet cible.
D'autres arguments sont une liste d'objets source.
Il copie les propriétés de tous les objets source dans la cible dest
, puis les renvoie comme résultat.
Par exemple, nous avons un objet user
, ajoutons-y quelques autorisations :
let user = { nom : "John" } ; laissez autorisations1 = { canView : true } ; let permissions2 = { canEdit : true } ; // copie toutes les propriétés de permissions1 et permissions2 dans l'utilisateur Object.assign(utilisateur, autorisations1, autorisations2); // maintenant utilisateur = { nom : "John", canView : true, canEdit : true } alert(utilisateur.nom); // John alerte (utilisateur.canView); // vrai alerte(user.canEdit); // vrai
Si le nom de propriété copié existe déjà, il est écrasé :
let user = { nom : "John" } ; Object.assign(user, { nom : "Pete" }); alert(utilisateur.nom); // maintenant utilisateur = { nom : "Pete" }
Nous pouvons également utiliser Object.assign
pour effectuer un simple clonage d'objet :
laissez l'utilisateur = { nom : "Jean", âge : 30 ans } ; let clone = Object.assign({}, utilisateur); alert(clone.nom); // John alert(clone.age); // 30
Ici, il copie toutes les propriétés de user
dans l'objet vide et le renvoie.
Il existe également d'autres méthodes pour cloner un objet, par exemple en utilisant la syntaxe spread clone = {...user}
, abordée plus loin dans le didacticiel.
Jusqu'à présent, nous avons supposé que toutes les propriétés de user
étaient primitives. Mais les propriétés peuvent être des références à d’autres objets.
Comme ça:
laissez l'utilisateur = { nom : "Jean", tailles : { hauteur : 182, largeur : 50 } } ; alerte( utilisateur.sizes.height ); // 182
Maintenant, il ne suffit plus de copier clone.sizes = user.sizes
, car user.sizes
est un objet et sera copié par référence, donc clone
et user
partageront les mêmes tailles :
laissez l'utilisateur = { nom : "Jean", tailles : { hauteur : 182, largeur : 50 } } ; let clone = Object.assign({}, utilisateur); alert( user.sizes === clone.sizes ); // vrai, même objet // tailles de partage utilisateur et clone utilisateur.sizes.width = 60 ; // change une propriété d'un seul endroit alerte(clone.sizes.width); // 60, récupère 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 devons 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 profond.
L'appel structuredClone(object)
clone l' object
avec toutes les propriétés imbriquées.
Voici comment nous pouvons l'utiliser dans notre exemple :
laissez l'utilisateur = { nom : "Jean", tailles : { hauteur : 182, largeur : 50 } } ; laissez clone = structuréClone(utilisateur); alert( user.sizes === clone.sizes ); // faux, objets différents // l'utilisateur et le clone n'ont plus aucun rapport maintenant utilisateur.sizes.width = 60 ; // change une propriété d'un seul endroit alerte(clone.sizes.width); // 50, sans rapport
La méthode structuredClone
peut cloner la plupart des types de données, tels que les objets, les tableaux et les 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:
laissez l'utilisateur = {} ; // créons une référence circulaire : // user.me fait référence à l'utilisateur lui-même utilisateur.me = utilisateur ; laissez clone = structuréClone(utilisateur); alert(clone.me === clone); // vrai
Comme vous pouvez le voir, clone.me
fait référence au clone
, pas à l' user
! La référence circulaire a donc également été clonée correctement.
Cependant, il existe des cas où structuredClone
échoue.
Par exemple, lorsqu'un objet possède une propriété de fonction :
// erreur structuréClone({ f: fonction() {} });
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.
Les objets sont attribués et copiés par référence. En d’autres termes, une variable ne stocke pas la « valeur de l’objet », mais une « référence » (adresse en mémoire) pour la valeur. Donc, copier une telle variable ou la transmettre comme argument de fonction copie cette référence, pas l'objet lui-même.
Toutes les opérations via des références copiées (comme l'ajout/suppression de propriétés) sont effectuées sur le même objet unique.
Pour créer une « copie réelle » (un clone), nous pouvons utiliser Object.assign
pour ce que l'on appelle la « copie superficielle » (les objets imbriqués sont copiés par référence) ou une fonction de « clonage profond » structuredClone
ou utiliser une implémentation de clonage personnalisée, telle que comme _.cloneDeep(obj).