Copie approfondie en JavaScript avec structuredClone

La plate-forme propose désormais une fonction structuréClone(), une fonction intégrée de copie profonde.

Pendant longtemps, vous avez dû 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 de copie profonde.

Navigateurs pris en charge

  • Chrome: 98 <ph type="x-smartling-placeholder">
  • Edge: 98 <ph type="x-smartling-placeholder">
  • Firefox: 94 <ph type="x-smartling-placeholder">
  • Safari: 15.4. <ph type="x-smartling-placeholder">

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 comme dans l'original.

Vous pouvez créer une copie superficielle en JavaScript à l'aide de l'opérateur de répartition 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} parcourt les propriétés (énumérables) de myOriginal à l'aide de l'opérateur de répartition. Elle utilise le nom et la valeur de la propriété, et les attribue une par une à un objet vide qui vient d'être 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 et les valeurs non primitives. Pour citer MDN:

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

MDN – Primitive

Les valeurs non primitives sont traitées comme des références. En d'autres termes, la copie d'une valeur consiste simplement à 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 une copie de cet objet. Cela peut être très important pour s'assurer que deux morceaux de code ne partagent pas accidentellement un objet et ne manipulent pas à votre insu l'état de l'autre.

Auparavant, il n'existait pas de méthode simple ni efficace pour créer une copie profonde d'une valeur en JavaScript. De nombreuses personnes utilisaient des bibliothèques tierces comme la fonction cloneDeep() de Lodash. La solution la plus courante à ce problème était sans doute un piratage basé sur JSON:

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

En fait, cette solution de contournement si populaire a permis à V8 d'optimiser de manière agressive JSON.parse(), en particulier le modèle ci-dessus, pour qu'il soit aussi rapide que possible. Bien que rapide, cette technologie présente quelques inconvénients et présente quelques trajectoires:

  • Structures de données récursives: JSON.stringify() est généré lorsque vous lui attribuez une structure de données récursive. Cela peut se produire assez facilement lorsque vous travaillez avec des listes ou des arborescences liées.
  • 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 la possibilité de créer des copies approfondies des valeurs JavaScript à plusieurs endroits: le stockage d'une valeur JS dans IndexedDB nécessite une forme de sérialisation afin de pouvoir être stockée sur le disque, puis dé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 cela s'appelle "Structured Clone" (Clone structurée). Jusqu'à récemment, il n'était pas facilement accessible aux développeurs.

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

const myDeepCopy = structuredClone(myOriginal);

Et voilà ! Il s'agit de l'API dans son ensemble. Pour en savoir plus, consultez l'article sur le Réseau Display de Google.

Fonctionnalités et limites

Le clonage structuré comble de nombreuses lacunes de la technique JSON.stringify() (mais pas toutes). Le clonage structuré peut gérer des structures de données cycliques, accepter de nombreux types de données intégrés, et est généralement plus robuste et souvent plus rapide.

Cependant, il présente encore certaines limites qui peuvent vous prendre au dépourvu:

  • Prototypes: si vous utilisez structuredClone() avec une instance de classe, vous obtiendrez un objet brut en retour. la valeur, car le clonage structuré élimine la chaîne de prototype 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. Il entraînera la génération de structuredClone().

Si l'une de ces limites est nécessaire à 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 correspondre à votre cas d'utilisation.

Performances

Je n'ai pas effectué de nouvelle comparaison de microbenchmarks, 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 très petits objets. Je m'attends à ce qu'il n'y ait pas de différence. Les techniques basées sur le clonage structuré étaient (nettement) plus rapides pour les objets plus volumineux. Étant donné que la nouvelle version de structuredClone() n'entraîne pas de frais généraux liés à l'utilisation abusive d'autres API et qu'elle est plus robuste que JSON.parse(), je vous recommande d'en faire votre approche par défaut pour créer des copies profondes.

Conclusion

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