Copie approfondie en JavaScript avec structuredClone

La plate-forme est désormais fournie avec "structuredClone()", une fonction intégrée de copie profonde.

Pendant longtemps, vous deviez recourir à des solutions de contournement et à des bibliothèques pour créer une copie approfondie d'une valeur JavaScript. La plate-forme est désormais fournie avec structuredClone(), une fonction intégrée pour la copie profonde.

Navigateurs pris en charge

  • Chrome : 98.
  • Edge : 98.
  • Firefox: 94
  • Safari : 15.4.

Source

Copies superficielles

La copie d'une valeur en JavaScript est presque toujours superficielle, par opposition à la copie profonde. Cela signifie que les modifications apportées aux valeurs profondément imbriquées seront visibles dans la copie et dans l'original.

Voici une façon de créer une copie superficielle en JavaScript à l'aide de l'opérateur de propagation d'objet ... :

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

L'ajout ou la modification d'une propriété directement dans la copie superficielle n'affecte que la copie, et non l'originale:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

Toutefois, l'ajout ou la modification d'une propriété profondément imbriquée affecte à la fois la copie et l'original :

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

L'expression {...myOriginal} itère sur les propriétés (énumérables) de myOriginal à l'aide de l'opérateur Spread. Il utilise le nom et la valeur de la propriété, et les attribue un par un à un objet vide nouvellement créé. Ainsi, l'objet obtenu est de forme identique, mais avec sa propre copie de la liste des propriétés et des valeurs. Les valeurs sont également copiées, mais les valeurs dites primitives sont gérées différemment par la valeur JavaScript que les valeurs non primitives. Pour citer le MDN:

En JavaScript, une primitive (valeur primitive, type de données primitives) désigne une donnée qui n'est pas un objet et ne possède aucune méthode. Il existe sept types de données primitifs: chaîne, nombre, bigint, booléen, undefined, symbole et null.

MDN : primitif

Les valeurs non primitives sont gérées en tant que références. Cela signifie que la copie de la valeur ne consiste en réalité qu'à copier une référence au même objet sous-jacent, ce qui entraîne le comportement de copie superficielle.

Copies profondes

L'inverse d'une copie superficielle est une copie profonde. Un algorithme de copie profonde copie également les propriétés d'un objet une par une, mais s'appelle de manière récursive lorsqu'il trouve une référence à un autre objet, créant ainsi également une copie de cet objet. Cela peut être très important pour s'assurer que deux extraits de code ne partagent pas accidentellement un objet et ne manipulent pas l'état de l'autre à leur insu.

Il n'existait auparavant aucun moyen simple ni pratique de créer une copie profonde d'une valeur en JavaScript. De nombreuses personnes ont utilisé des bibliothèques tierces telles que la fonction cloneDeep() de Lodash. La solution la plus courante à ce problème était un hack basé sur JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

En fait, cette solution de contournement était si populaire que V8 a optimisé de manière agressive JSON.parse(), et plus particulièrement le modèle ci-dessus, pour le rendre aussi rapide que possible. Bien qu'il soit rapide, il présente quelques inconvénients et pièges:

  • Structures de données récursives: JSON.stringify() génère une exception lorsque vous lui transmettez une structure de données récursive. Cela peut se produire assez facilement lorsque vous travaillez avec des listes ou des arbres liés.
  • Types intégrés: JSON.stringify() est généré si la valeur contient d'autres composants JS intégrés tels que Map, Set, Date, RegExp ou ArrayBuffer.
  • Fonctions: JSON.stringify() supprime discrètement les fonctions.

Clonage structuré

La plate-forme avait déjà besoin de pouvoir créer des copies profondes des valeurs JavaScript à plusieurs endroits: stocker une valeur JS dans IndexedDB nécessite une forme de sérialisation afin qu'elle puisse être stockée sur disque et sérialisée ultérieurement pour restaurer la valeur JS. De même, l'envoi de messages à un WebWorker via postMessage() nécessite de transférer une valeur JS d'un domaine JS à un autre. L'algorithme utilisé pour ce processus est appelé "clone structuré". Jusqu'à récemment, il n'était pas facilement accessible aux développeurs.

Ça a changé ! La spécification HTML a été modifiée pour exposer une fonction appelée structuredClone() qui exécute exactement cet algorithme afin que les développeurs puissent facilement créer des copies profondes des valeurs JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Et voilà ! C'est l'intégralité de l'API. Pour en savoir plus, consultez l'article MDN.

Fonctionnalités et limites

Le clonage structuré résout de nombreux (mais pas tous) des inconvénients de la technique JSON.stringify(). Le clonage structuré peut gérer des structures de données cycliques, prendre en charge de nombreux types de données intégrés, et est généralement plus robuste et souvent plus rapide.

Toutefois, il présente encore certaines limites qui peuvent vous surprendre:

  • Prototypes: si vous utilisez structuredClone() avec une instance de classe, vous obtiendrez un objet simple comme valeur renvoyée, car le clonage structuré supprime la chaîne de prototypes de l'objet.
  • Fonctions: si votre objet contient des fonctions, structuredClone() génère une exception DataCloneError.
  • Non clonables: certaines valeurs ne peuvent pas être clonées de manière structurée, en particulier les nœuds Error et DOM. structuredClone() sera alors généré.

Si l'une de ces limites est rédhibitoire pour votre cas d'utilisation, des bibliothèques telles que Lodash fournissent toujours des implémentations personnalisées d'autres algorithmes de clonage profond qui peuvent ou non convenir à votre cas d'utilisation.

Performances

Je n'ai pas effectué de nouvelle comparaison de micro-benchmarks, mais j'ai effectué une comparaison début 2018, avant que structuredClone() ne soit exposé. À l'époque, JSON.parse() était l'option la plus rapide pour les objets très petits. Je m'attends à ce que cela reste le cas. Les techniques qui reposaient sur le clonage structuré étaient (considérablement) plus rapides pour les objets plus volumineux. Étant donné que la nouvelle structuredClone() ne comporte pas les frais généraux liés à l'utilisation abusive d'autres API et qu'elle est plus robuste que JSON.parse(), nous vous recommandons de l'utiliser par défaut pour créer des copies profondes.

Conclusion

Si vous devez créer une copie profonde d'une valeur en JS (par exemple, si vous utilisez des structures de données immuables ou si vous voulez vous assurer qu'une fonction peut manipuler un objet sans affecter l'original), vous n'avez plus besoin de recourir à des solutions de contournement ou à des bibliothèques. L'écosystème JavaScript comprend désormais structuredClone(). Hourra.