Deep-copying in JavaScript utilizzando structuredClone

La piattaforma ora include structuredClone(), una funzione integrata per il deep-copy.

Per molto tempo si è dovuto ricorrere a soluzioni alternative e librerie per creare una copia approfondita di un valore JavaScript. La piattaforma ora viene fornita con structuredClone(), una funzione integrata per il deep-copy.

Supporto dei browser

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

Origine

Copie superficiali

La copia di un valore in JavaScript è quasi sempre shallow, anziché deep. Ciò significa che le modifiche ai valori che presentano un alto grado di nidificazione saranno visibili sia nel testo 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à profondamente nidificata influisce su sia la copia sia l'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. Usa 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 in modo diverso dal valore JavaScript rispetto ai valori non primitivi. Per citare 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, non definito, simbolo e nullo.

MDN - Primitivo

I valori non primitivi vengono gestiti come riferimenti, il che significa che l'atto di copiare il valore consiste in realtà nella copia di un riferimento allo stesso oggetto sottostante, il che si traduce in un comportamento di copia superficiale.

Copie profonde

L'opposto di un testo di base è una copia approfondita. Un algoritmo di deep copy copia anche le proprietà di un oggetto una per una, ma si attiva in modo ricorsivo quando trova un riferimento a un altro oggetto, creando anche una copia di quell'oggetto. Questo può essere molto importante per assicurarsi che due porzioni di codice non condividano accidentalmente un oggetto e manipolano inconsapevolmente lo stato degli altri.

Non esisteva un modo semplice o piacevole per creare una copia approfondita di un valore in JavaScript. Molte persone si sono affidate a librerie di terze parti, come la funzione Lodash cloneDeep(). Probabilmente la soluzione più comune a questo problema era un attacco di pirateria informatica basato su JSON:

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

In effetti, questa era una soluzione alternativa così popolare che V8 ha ottimizzato in modo aggressivo JSON.parse() e nello specifico il pattern precedente per renderlo 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 elenchi collegati o strutture ad albero.
  • Tipi integrati: JSON.stringify() verrà restituito se il valore contiene altri elementi JS integrati come Map, Set, Date, RegExp o ArrayBuffer.
  • Funzioni: JSON.stringify() eliminerà in modo discreto le funzioni.

Clonazione strutturata

La piattaforma aveva già bisogno della capacità di creare copie profonde dei valori JavaScript in un paio di punti: l'archiviazione di un valore JS in IndexedDB richiede una qualche forma di serializzazione in modo che possa essere archiviato su disco e successivamente deserializzato per ripristinare il valore JS. Allo stesso modo, l'invio di messaggi a un WebWorker tramite postMessage() richiede il trasferimento di un valore JS da un'area di autenticazione JS a un'altra. L'algoritmo utilizzato a 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! ossia l'intera API. Per informazioni più dettagliate, consulta l'articolo su MDN.

Funzionalità e limitazioni

La clonazione strutturata risolve molti (anche se non tutti) i problemi 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, ha comunque alcune limitazioni che potrebbero coglierti di sorpresa:

  • Prototipi: se utilizzi structuredClone() con un'istanza di classe, verrà restituito un oggetto semplice poiché la clonazione strutturata scarta la catena di prototipi dell'oggetto.
  • Funzioni: se l'oggetto contiene funzioni, structuredClone() genererà un'eccezione DataCloneError.
  • Non clonabili: alcuni valori non sono strutturati clonabili, in particolare Error e nodi DOM. it causerà il lancio di structuredClone().

Se alcune di queste limitazioni costituiscono un elemento decisivo per il tuo caso d'uso, 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 fatto un nuovo confronto di micro-benchmark, ho fatto un confronto all'inizio del 2018, prima che structuredClone() fosse esposto. All'epoca JSON.parse() era l'opzione più veloce per gli oggetti molto piccoli. Prevedo che rimanga invariato. Le tecniche che si basavano sulla clonazione strutturata erano (molto più veloci) più veloci per gli oggetti più grandi. Considerato che il nuovo structuredClone() non richiede l'uso eccessivo di altre API ed è più efficace di JSON.parse(), ti consiglio di impostarlo come approccio predefinito per la creazione di copie profonde.

Conclusione

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