Copie approfondie en JavaScript avec structuredClone

La plate-forme intègre désormais la fonction "StructuredClone()" pour la copie approfondie.

Pendant la longue période où vous avez dû recourir à des solutions de contournement et à des bibliothèques, vous avez dû créer une copie complète d'une valeur JavaScript. La plate-forme intègre désormais structuredClone(), une fonction intégrée de copie approfondie.

Navigateurs pris en charge

  • 98
  • 98
  • 94
  • 15,4

Source

Copies superficielles

Copier une valeur en JavaScript est presque toujours faible, contrairement à une valeur profonde. Cela signifie que les modifications apportées aux valeurs profondément imbriquées sont visibles dans la copie et dans l'original.

Vous pouvez créer une copie superficielle en JavaScript à l'aide de l'opérateur de propagation de l'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'original:

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 présente une 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 traitées différemment par la valeur JavaScript des valeurs non primitives. Pour citer MDN:

En JavaScript, une primitive (valeur primitive, type de données primitif) est 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 valeur nulle.

MDN – Primitive

Les valeurs non primitives sont traitées comme des references, ce qui signifie que la copie de la valeur consiste en fait à copier une référence au même objet sous-jacent, ce qui génère un comportement de copie superficiel.

Copies profondes

Le contraire 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 extraits de code ne partagent pas accidentellement un objet et manipulent à votre insu l'état de l'autre.

Auparavant, il n'existait aucun moyen simple ou pratique de créer une copie profonde d'une valeur en JavaScript. De nombreuses personnes s'appuyaient sur des bibliothèques tierces telles que la fonction cloneDeep() de Lodash. La solution la plus courante à ce problème était sans doute le piratage basé sur JSON:

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

En fait, il s'agissait d'une solution de contournement si populaire que V8 a optimisé de manière agressive JSON.parse() et en particulier le modèle ci-dessus pour le rendre aussi rapide que possible. Bien qu'il soit rapide, il présente quelques défauts et fils électriques:

  • 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 associées.
  • Types intégrés: JSON.stringify() est généré si la valeur contient d'autres éléments JS intégrés tels que Map, Set, Date, RegExp ou ArrayBuffer.
  • Functions (Fonctions) : JSON.stringify() supprime les fonctions en mode silencieux.

Clonage structuré

La plate-forme avait déjà besoin de la possibilité de créer des copies profondes des valeurs JavaScript à plusieurs endroits: le stockage d'une valeur JS dans IndexedDB nécessite une forme de sérialisation afin qu'elle puisse être stockée sur disque, puis désérialisée pour restaurer la valeur JS. De même, l'envoi de messages à un WebWorker via postMessage() nécessite le transfert d'une valeur JS d'un domaine JS à un autre. L'algorithme utilisé pour cela s'appelle le "clonage 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 approfondies des valeurs JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Et voilà ! C'est l'API complète. Pour en savoir plus, consultez l'article sur MN.

Fonctionnalités et limites

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

Cependant, elle présente encore certaines limites qui peuvent vous méfier:

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

Si l'une de ces limites vous empêche de trouver une solution à votre cas d'utilisation, des bibliothèques telles que Lodash fournissent tout de même 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 avec des 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 que cela reste le même. Les techniques reposant sur le clonage structuré étaient (nettement) plus rapides pour les objets plus volumineux. Étant donné que la nouvelle version de structuredClone() ne nécessite pas 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 devez créer une copie profonde d'une valeur dans JavaScript, par exemple parce que vous utilisez des structures de données immuables ou si vous souhaitez vous assurer qu'une fonction peut manipuler un objet sans affecter l'original, vous n'avez plus besoin de chercher des solutions de contournement ou des bibliothèques. L'écosystème JavaScript comporte désormais structuredClone(). Hourra !