Introduction
Le Javascript est un langage souple qui peut paraître facile au premier abord. Mais je vous le dis : en réalité, il recèle de subtilités qui peuvent rapidement mettre le bazar si on ne les maîtrise pas !
Une de ces subtilités dont on va parler aujourd'hui, c'est le clonage d'objets. Et pour changer un peu des moutons, on va cloner des objets robots (n'en déplaise à Will Smith, alias Del Spooner) ! C'est parti !
Le clonage naïf
Partons du principe que l'on dispose d'une instance d'un premier prototype de robot, et que l'on veuille alors le cloner dans une production. Ici, on représentera une production par un tableau production
contenant nos robots :
(Notez que pour aider à modéliser le problème, j'utiliserai du Typescript)
Résultat de la console:
------- Production check -------
robot 1 = {id: 10, isConform: true}
robot 2 = {id: 10, isConform: true}
robot 3 = {id: 10, isConform: true}
robot 4 = {id: 10, isConform: true}
...
Quoi ?! Mais ils ont tous les mêmes identifiants ! 😱
Si vous le souhaitez, vous pouvez vous exercer à débugger le code ci-dessus en ajoutant du code et des logs.
Et en effet, il y a bien un problème dans cette production : en JS, l'opérateur =
n'effectue Pas de copie d'un objet ou d'un array à proprement parler, mais uniquement de la référence de cet objet en question.
Ainsi, const newRobot = robotProto;
ne crée pas un nouvel objet mais référence le robot proto. A chaque tour de boucle.
Du coup, sans le savoir, on a toujours affecté le même objet dans toutes les cases du tableau, et par conséquent modifié le même objet à chaque itération. Au final, on se retrouve évidemment avec la dernière valeur enregistrée : 10.
Cette explication est d'ailleurs vérifiable avec l'opérateur de comparaison ===
:
// 2 contenus identiques mais dans des objets différents
const robot1 = { id: 0, isConform: true };
const robot2 = { id: 0, isConform: true };
console.log(robot1 === robot2); // false => pas les mêmes objets
//...et si on revient à notre production râtée
console.log(production[0] === production[9]); // true => c'est le même objet !
Bon, comment corriger cette production ?
Nouvel objet et spread operator
Une solution consiste à utiliser le spread operator. Combiné aux accolades {}
pour déclarer un nouvel objet, le spread operator ...
permet de copier tous les champs de l'objet robotProto
. Très efficace si l'on a un paquet de champs à copier d'un coup !
Résultat de la console :
------- Production check -------
robot 1 = {id: 1, isConform: true}
robot 2 = {id: 2, isConform: true}
robot 3 = {id: 3, isConform: true}
robot 4 = {id: 4, isConform: true}
...
Aah c'est mieux !
Dans notre cas, cette solution minimaliste convient parfaitement !
Sauf que cette technique a des limites... En effet, le spread operator souffre du même défaut que l'opérateur =
: il ne clone pas réellement les champs qui sont de type objets et arrays 😟
Ainsi, si l'on enrichit la structure de données avec des objets à plusieurs niveaux, on retombe sur le problème des références aux niveaux plus profonds :
interface Robot {
id: number;
isConform: boolean;
head: {
eyesColor: string;
};
body: {
material: string;
};
}
const complexRobot: Robot = {
id: 0,
isConform: true,
head: {
eyesColor: 'blue'
};
body: {
material: 'carbon'
};
};
const badClone = {...complexRobot};
console.log(badClone.head === complexRobot.head);//true => on a encore le même objet
La méthode stringify
Une solution consiste à transformer un objet en string, puis le re-parser successivement :
const newRobot = JSON.parse(JSON.stringify(complexRobot));
Cela peut paraître empirique, je vous l'accorde. Pourtant dans beaucoup de cas, c'est le meilleur équilibre entre fiabilité et rapidité d'exécution dans la catégorie du clonage en profondeur.
Gros warning cependant : du fait de la transformation intermédiaire en string, les types correctement pris en charge sont très limités. Les fonctions par exemple seront tout simplement perdues !
structuredClone
La fonction structuredClone est une nouveauté supportée par tous les principaux navigateurs depuis début 2022. Elle permet de cloner automatiquement et en profondeur tout le contenu d'un objet :
const newRobot = structuredClone(complexRobot);
L'avantage par rapport au JSON.stringify
, c'est qu'elle prend en charge plus d'objets spéciaux comme les Date, RegExp ou Set, mais toujours pas de prise en charge de types spéciaux comme les fonctions. Voir ici la liste exhaustive des types supportés.
Toute puissance ayant un coût, c'est également la méthode standard la plus lente à exécuter pour le clonage.
L'historique Object.assign
Parce qu'il faut respecter les anciens 😄, je terminerai cette liste en évoquant Object.assign
.
Très similaire au spread operator, il a l'avantage de pouvoir modifier un objet déjà instancié tout en se conformant au type d'origine et à ses méthodes lorsque c'est une classe. Personnellement, je trouve qu'avec le Typescript cette méthode a moins de cas d'usage, car on va utiliser des types (comme l'interface
) qui vont déjà nous aider à garantir la conformité d'une structure de données avec ses attributs et ses fonctions.
// usage basique dans notre cas
const newRobot = Object.assign({}, complexRobot);
Mais tout comme le spread operator, il ne clone qu'au premier niveau, et ne copie que les références des objets et arrays.
Conclusion
De manière générale, il ne faut cloner que s'il y a un réel besoin. Si par défaut le clonage des objets et arrays n'est pas automatique, c'est qu'il y a une raison : l'optimisation.
Mais dans le cas où le clonage est nécessaire, il vaut mieux choisir :
- le spread operator pour une copie à un seul niveau (sans objets ou arrays)
- l'
Object.assign
pour une assignation à un seul niveau (sans objets ou arrays) plutôt pour des classes - le JSON parse/stringify pour une copie en profondeur rapide (mais sans objets spéciaux)
- le structuredClone pour une copie en profondeur plus complète mais plus lente
Et si aucune de ces méthodes standard n'est satisfaisante, je vous invite à chercher du côté d'une librairie comme lodash, ou à implémenter votre propre méthode de clonage adaptée à votre cas d'usage !
Comme toujours, il faut s'adapter à chaque situation...