Javascript: on clone des robots !
Tags: javascript, clone, object, typescript
Published: 2023-04-12 15:02:11

## 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 !

![Binome Will Smith scientifique](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ib30ew8lq614fo528t0c.jpg)

## 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 :

{% embed https://stackblitz.com/edit/xbxa8n?devToolsHeight=33&embed=1&file=index.ts&hideExplorer=1&hideNavigation=1&theme=dark %}

(Notez que pour aider à modéliser le problème, j'utiliserai du [Typescript](https://www.typescriptlang.org/))

Résultat de la console:

```log
------- 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.

![Will Smith devant clones](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/quwvsanrj5n46ud9jozj.jpg)

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 `===` :

```tsx
// 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 !

{% embed https://stackblitz.com/edit/xbxa8n-jn7qhc?ctl=1&devToolsHeight=33&embed=1&file=index.ts&hideExplorer=1&hideNavigation=1&theme=dark %}

Résultat de la console :

```log
------- 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 :

```tsx
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 :

```tsx
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](https://developer.mozilla.org/en-US/docs/Web/API/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 :

```tsx
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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types) 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.

```tsx
// 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](https://lodash.com/docs/#cloneDeep), ou à implémenter votre propre méthode de clonage adaptée à votre cas d'usage !

Comme toujours, il faut s'adapter à chaque situation...

![Will Smith posé](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/la7u1w0bjklshcc7mo4v.jpg)