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 interessa solo la copia, non l'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 sia 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à (enumerabili) 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, la forma dell'oggetto risultante è identica, ma con la 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 elemento primitivo (valore primitivo, tipo di dati primitivi) è costituito da dati che non sono un oggetto e non dispongono di 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 quell'oggetto. 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. Anche se è veloce, presenta un paio di inconvenienti e svantaggi:

  • Strutture di dati ricorsive: JSON.stringify() viene generata quando gli assegni una struttura di dati ricorsivi. Questo può accadere abbastanza facilmente quando si utilizzano liste o alberi collegati.
  • Tipi incorporati: JSON.stringify() verrà lanciato 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 di poter 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 denominata structuredClone() che esegue esattamente quell'algoritmo come mezzo per consentire agli sviluppatori di creare facilmente copie profonde 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 è in grado di gestire strutture di dati cicliche, supportare molti tipi di dati integrati 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 alcune di queste limitazioni costituiscono un elemento decisivo per il tuo caso d'uso, le librerie come Lodash forniscono comunque implementazioni personalizzate di altri algoritmi di clonazione approfondita che potrebbero o meno essere adatte 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 dell'esposizione di structuredClone(). All'epoca JSON.parse() era l'opzione più veloce per gli 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à.