Deep-copying in JavaScript utilizzando structuredClone

La piattaforma ora include structuredClone(), una funzione incorporata per la copia approfondita.

Per molto tempo, è stato necessario ricorrere a soluzioni alternative e librerie per creare una copia approfondita di un valore JavaScript. La piattaforma ora include structuredClone(), una funzione integrata per la copia approfondita.

Supporto dei browser

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

Origine

Copie superficiali

La copia di un valore in JavaScript è quasi sempre superficiale, anziché profonda. Ciò significa che le modifiche ai valori nidificati in modo profondo saranno visibili sia nella copia che nell'originale.

Un modo per creare una copia superficiale in JavaScript utilizzando l'operatore di diffusione degli oggetti ...:

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

const myShallowCopy = {...myOriginal};

L'aggiunta o la modifica di una proprietà direttamente nella copia superficiale influisce solo sulla copia, non sull'originale:

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

Tuttavia, l'aggiunta o la modifica di una proprietà nidificata in modo profondo influisce sia sulla copia che sull'originale:

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

L'espressione {...myOriginal} esegue l'iterazione sulle proprietà (elencabili) di myOriginal utilizzando l'operatore di distribuzione. Utilizza il nome e il valore della proprietà e li assegna uno alla volta a un oggetto vuoto appena creato. Di conseguenza, l'oggetto risultante ha la stessa forma, ma con una propria copia dell'elenco di proprietà e valori. Anche i valori vengono copiati, ma i cosiddetti valori primitivi vengono gestiti dal valore JavaScript in modo diverso rispetto ai valori non primitivi. Citando il MDN:

In JavaScript, un valore primitivo (valore primitivo, tipo di dati primitivi) è un dato che non è un oggetto e non ha metodi. Esistono sette tipi di dati primitivi: stringa, numero, bigint, booleano, undefined, simbolo e null.

MDN - Primitive

I valori non primitivi vengono gestiti come riferenze, il che significa che l'atto di copiare il valore è in realtà solo la copia di un riferimento allo stesso oggetto sottostante, con il conseguente comportamento di copia superficiale.

Copie approfondite

L'opposto di una copia superficiale è una copia profonda. Un algoritmo di copia profonda copia anche le proprietà di un oggetto una alla volta, ma si richiama in modo ricorsivo quando trova un riferimento a un altro oggetto, creando anche una copia di quest'ultimo. Questo può essere molto importante per assicurarti che due parti di codice non condividano accidentalmente un oggetto e manipolazioni inconsapevolmente lo stato l'uno dell'altro.

In passato non esisteva un modo semplice o pratico per creare una copia approfondita di un valore in JavaScript. Molte persone si affidavano a librerie di terze parti come la funzione cloneDeep() di Lodash. Probabilmente la soluzione più comune a questo problema era un hack basato su JSON:

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

In effetti, si trattava di una soluzione alternativa così popolare che V8 l'ha ottimizzata in modo aggressivo JSON.parse() e in particolare il pattern riportato sopra per renderla il più veloce possibile. Sebbene sia veloce, presenta alcuni inconvenienti e rischi:

  • Strutture di dati ricorsive: JSON.stringify() viene lanciato quando viene fornita una struttura di dati ricorsiva. Questo può accadere abbastanza facilmente quando si utilizzano liste o alberi collegati.
  • Tipi incorporati: JSON.stringify() viene generato se il valore contiene altri elementi incorporati JS come Map, Set, Date, RegExp o ArrayBuffer.
  • Funzioni: JSON.stringify() eliminerà silenziosamente le funzioni.

Clonazione strutturata

La piattaforma aveva già bisogno della possibilità di creare copie complete dei valori JavaScript in un paio di punti: la memorizzazione di un valore JS in IndexedDB richiede una qualche forma di serializzazione in modo che possa essere memorizzato su disco e successivamente deserializzato per ripristinare il valore JS. Analogamente, l'invio di messaggi a un WebWorker tramite postMessage() richiede il trasferimento di un valore JS da un ambito JS a un altro. L'algoritmo utilizzato per questo scopo si chiama "Clone strutturato" e fino a poco tempo fa non era facilmente accessibile agli sviluppatori.

Ora le cose sono cambiate. La specifica HTML è stata modificata per esporre una funzione chiamata structuredClone() che esegue esattamente quell'algoritmo per consentire agli sviluppatori di creare facilmente copie approfondite dei valori JavaScript.

const myDeepCopy = structuredClone(myOriginal);

È tutto! Questa è l'intera API. Per maggiori dettagli, consulta l'articolo MDN.

Funzionalità e limitazioni

La clonazione strutturata risolve molti (anche se non tutti) i difetti della tecnica JSON.stringify(). La clonazione strutturata può gestire strutture di dati cicliche, supportare molti tipi di dati incorporati ed è generalmente più solida e spesso più veloce.

Tuttavia, presenta ancora alcune limitazioni che potrebbero sorprenderti:

  • Prototipi: se utilizzi structuredClone() con un'istanza di classe, otterrai un oggetto normale come valore di ritorno, poiché la clonazione strutturata ignora la catena di prototipi dell'oggetto.
  • Funzioni: se l'oggetto contiene funzioni, structuredClone() lancerà un'eccezione DataCloneError.
  • Non clonabili: alcuni valori non sono strutturati e non possono essere clonati, in particolare Error e i nodi DOM. Ciò causerà l'emissione di un'eccezione da parte di structuredClone().

Se una di queste limitazioni è un fattore decisivo per il tuo caso d'uso, librerie come Lodash forniscono comunque implementazioni personalizzate di altri algoritmi di clonazione profonda che potrebbero o meno essere adatti al tuo caso d'uso.

Prestazioni

Anche se non ho eseguito un nuovo confronto di micro-benchmark, ne ho eseguito uno all'inizio del 2018, prima che structuredClone() fosse esposto. All'epoca, JSON.parse() era l'opzione più veloce per oggetti molto piccoli. Mi aspetto che rimanga invariato. Le tecniche basate sulla clonazione strutturata erano (significativamente) più veloci per gli oggetti più grandi. Dato che il nuovo structuredClone() non comporta l'overhead dell'uso improprio di altre API ed è più solido di JSON.parse(), ti consiglio di utilizzarlo come approccio predefinito per la creazione di copie profonde.

Conclusione

Se devi creare una copia approfondita di un valore in JS, ad esempio perché utilizzi strutture di dati immutabili o vuoi assicurarti che una funzione possa manipolare un oggetto senza influire sull'originale, non devi più ricorrere a soluzioni alternative o librerie. L'ecosistema JS ora ha structuredClone(). Urrà.