Deep-copying in JavaScript utilizzando structuredClone

La piattaforma ora viene fornita con strutturatoClone(), una funzione integrata per il deep-copy.

Per molto tempo, hai 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

  • 98
  • 98
  • 94
  • 15,4

Fonte

Copie superficiali

La copia di un valore in JavaScript è quasi sempre shallow, al contrario di deep. Ciò significa che le modifiche ai valori molto nidificati saranno visibili sia nel testo che nell'originale.

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

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 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 diffusione. Utilizza il nome e il valore della proprietà e li assegna uno alla volta a un oggetto vuoto appena creato. Pertanto, l'oggetto risultante ha una forma identica, ma con una propria copia dell'elenco di proprietà e valori. Anche i valori vengono copiati, ma i cosiddetti valori primitivi sono gestiti in modo diverso dal valore JavaScript rispetto ai valori non primitivi. Per citare MDN:

In JavaScript, un valore primitivo (valore primitivo, tipo di dati primitivi) è costituito da dati che non sono un oggetto e non hanno metodi. Esistono sette tipi di dati primitivi: stringa, numero, bigint, booleano, non definito, simbolo e null.

MDN - Primitivo

I valori non primitivi vengono gestiti come references, il che significa che l'atto di copiare il valore consiste davvero nella copia di un riferimento allo stesso oggetto sottostante, con conseguente comportamento di copia superficiale.

Copie approfondite

L'opposto di una copia superficiale è una copia estesa. Un algoritmo di deep copy copia anche le proprietà di un oggetto una per una, 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 assicurarsi che due parti di codice non condividano accidentalmente un oggetto e manipolino inconsapevolmente lo stato degli altri.

Un tempo non esisteva un modo semplice o simpatico per creare una copia approfondita di un valore in JavaScript. Molte persone si sono affidate a librerie di terze parti come la funzione cloneDeep() di Lodash. Probabilmente la soluzione più comune a questo problema è stata una compromissione basata su JSON:

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

In effetti, si trattava di una soluzione alternativa così popolare, quella della V8 ottimizzata in modo aggressivo JSON.parse() e nello specifico il pattern riportato sopra per renderla il più veloce possibile. Sebbene sia veloce, presenta alcune carenze e alcuni problemi:

  • Strutture di dati ricorsive: JSON.stringify() genera una struttura di dati ricorsiva. Questo può accadere molto facilmente quando si lavora con elenchi o alberi collegati.
  • Tipi integrati: JSON.stringify() viene generato se il valore contiene altri elementi JS integrati come Map, Set, Date, RegExp o ArrayBuffer.
  • Funzioni: JSON.stringify() eliminerà le funzioni in modo discreto.

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. Analogamente, l'invio di messaggi a WebWorker tramite postMessage() richiede il trasferimento di un valore JS da un'area di autenticazione JS a un altro. L'algoritmo utilizzato per questo processo è chiamato "clone strutturato" e fino a poco tempo fa non era facilmente accessibile per gli 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 complete dei valori JavaScript.

const myDeepCopy = structuredClone(myOriginal);

È tutto! Questa è l'intera API. Per informazioni più dettagliate, consulta l'articolo su MDN.

Funzionalità e limitazioni

La clonazione strutturata risolve molti (anche se non tutti) difetti della tecnica JSON.stringify(). La clonazione strutturata è in grado di gestire strutture di dati ciclici, 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 semplice come valore restituito, poiché la clonazione strutturata ignora la catena di prototipi dell'oggetto.
  • Funzioni: se l'oggetto contiene funzioni, structuredClone() genererà un'eccezione DataCloneError.
  • Non clonabili: alcuni valori non sono clonati strutturati, in particolare i nodi Error e DOM. In questo modo, viene generato il valore structuredClone().

Se una di queste limitazioni ti consente di infrangere un accordo per il tuo caso d'uso, librerie come Lodash forniscono comunque implementazioni personalizzate di altri algoritmi di clonazione profonda che potrebbero o meno adattarsi al tuo caso d'uso.

Esibizione

Anche se non ho effettuato un nuovo confronto di micro-benchmark, ho effettuato un confronto all'inizio del 2018, prima dell'esposizione di structuredClone(). All'epoca, JSON.parse() era l'opzione più veloce per gli oggetti molto piccoli. Prevedo che rimanga invariato. Le tecniche che si sono basate sulla clonazione strutturata sono state (molto più veloci) per gli oggetti più grandi. Considerando che la nuova structuredClone() non comporta l'overhead associato all'abuso di altre API ed è più efficace di JSON.parse(), ti consiglio di impostarlo come approccio predefinito per la creazione di copie approfondite.

Conclusione

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