Prima di parlare della clonazione degli oggetti in JavaScript è utile ricordare che esiste una differenza fondamentale tra essi e i tipi primitivi. Il passaggio di questi ultimi avviene infatti per valore mentre gli oggetti, così come gli array, vengono passati per riferimento. Ciò accade perché i tipi primitivi sono immutabili e il loro valore non può essere modificato. Oggetti e array sono invece mutabili e permettono l'alterazione dei valori a loro associati. Quando per esempio si passa un oggetto ad una funzione e lo si modifica all'interno di essa, i cambiamenti si riflettono anche sull'oggetto originale.
Un semplice caso di passaggio per riferimento con alterazione dei valori potrebbe essere il seguente:
let simpson = { bart: 10, lisa: 8 };
let copiaSimpson = simpson;
copiaSimpson.lisa = 9;
In seguito all'esecuzione di questa espressione abbiamo
simpson = { bart: 10, lisa: 9 }
e
copiaSimpson = { bart: 10, lisa: 9 }
Come possiamo osservare, dopo il passaggio per riferimento la modifica a carico di una variabile influisce anche sull'altra perché l'oggetto a cui fanno riferimento è il medesimo. In parole povere, copiaSimpson
punta allo stesso oggetto di simpson
. Non si ha quindi una copia indipendente.
Tale comportamento non è di per sé negativo ma in alcuni casi può rappresentare un problema, tutto dipende dal risultato che si vuole ottenere. Se vogliamo evitarlo la soluzione prende il nome di clonazione degli oggetti.
Cosa è la clonazione degli oggetti in JavaScript
In base a quanto anticipato è possibile sottolineare che tale pratica risulta utile soprattutto quando si desidera creare una copia di un oggetto senza modificarne l'originale. In linea generale esistono due tipi di clonazione: quella superficiale e quella profonda. A seconda del tipo scelto è possibile sfruttare dei metodi di clonazione differenti.
Con la clonazione superficiale viene generato un nuovo oggetto che contiene le stesse proprietà di quello originale. Se tali proprietà sono a loro volta degli oggetti i riferimenti rimangono i medesimi. In pratica, quando si applica una modifica ad una proprietà dell'oggetto clonato che è un riferimento ad un oggetto, essa si rifletterà anche sull'originale (e viceversa). La clonazione profonda produce invece una copia del tutto indipendente dell'originale e di tutti gli oggetti nidificati al suo interno. Ciò significa che le modifiche apportate all'oggetto clonato non influenzeranno l'originale (o viceversa).
Detto questo, analizziamo i diversi approcci alla clonazione che sono possibili in JavaScript. A questo proposito il linguaggio offre infatti buon numero di alternative.
La clonazione degli oggetti con Object.assign()
Tra le varie procedure disponibili, una delle più semplici prevede di sfruttare il metodo Object.assign()
. Quest'ultimo permette di copiare i valori di tutte le proprietà enumerabili da uno o più oggetti di origine, o "oggetti sorgente", in un oggetto di destinazione, o "oggetto target". Quando impiegato esso restituisce l'oggetto di destinazione dopo aver generato la copia. Un semplice esempio di clonazione degli oggetti basata su Object.assign()
potrebbe essere il seguente:
const oggettoSorgente = { nome: 'Apu', cognome: 'Nahasapeemapetilon' };
const oggettoTarget = Object.assign({}, oggettoSorgente);
Nel caso del codice proposto in precedenza Object.assign()
accetta come primo argomento un oggetto di destinazione vuoto rappresentato dalle parentesi graffe ("{}
"). Fatto questo assegna ad esso le proprietà di oggettoSorgente
generando una copia chiamata oggettoTarget
. Nello stesso modo, se si avessero più oggetti di origine sarebbe possibile passarli come argomenti addizionali al metodo.
Abbiamo inoltre un primo esempio di clonazione superficiale. Se infatti oggettoSorgente
contenesse delle proprietà annidate che fossero oggetti o array, esse non sarebbero clonate in profondità. Verrebbe creata invece una nuova copia delle referenze a quegli oggetti o array. Quindi, se una delle proprietà dell'originale venisse modificata tramite l'oggetto clonato, tale modifica si rifletterebbe anche in oggettoSorgente
a causa dei riferimenti esistenti.
Clonazione degli oggetti con l'operatore di spread
In JavaScript l'operatore di spread, rappresentato da tre punti in sequenza ("...
"), è un operatore unario che espande i valori di un'array, di un oggetto o di una stringa in punti di dati individuali. Consente in pratica di utilizzare i valori di un array o di un oggetto quando sono attesi più argomenti o elementi. Se per esempio un oggetto rappresenta un insieme di dati, l'operatore ne accetta le proprietà distribuendole singolarmente dove è necessario. Quindi, dato un oggetto {x: 1, y: 2}
, l'operatore distribuirà separatamente x
ed y
. Un suo impiego nella clonazione degli oggetti potrebbe essere esemplificato nel codice proposto di seguito:
const oggettoBar = { nome: 'Boe', cognome: 'Szyslak' };
const cloneBar = { ...oggettoBar };
Nell'esempio l'operatore di spread viene introdotto per creare una nuova copia dell'oggetto chiamato oggettoBar
. In questo modo esso espande gli elementi di oggettoBar
in un nuovo oggetto generando una copia separata chiamata cloneBar
. Così come con l'esempio basato su Object.assign()
, anche in questo caso clonazione avviene senza modificare direttamente l'oggetto originale.
Anche in questo caso la clonazione avviene però superficialmente e se le proprietà di oggettoBar
fossero a loro volta oggetti o array essi non verrebbero clonati.
Clonazione di oggetti in JavaScript con Lodash
Riprendendo l'esempio proposto in precedenza a proposito dell'operatore di spread possiamo evidenziare i limiti di una clonazione superficiale in questo modo:
const oggettoBar = { nome: 'Boe', cognome: 'Szyslak', lavoro: ['aprire il bar' , 'versare da bere'] };
const cloneBar = { ...oggettoBar };
cloneBar.lavoro.push('pulire il bancone');
console.log(oggettoBar);
L'output del codice sarà:
{ nome: 'Boe', cognome: 'Szyslak', lavoro: ['aprire il bar' , 'versare da bere', 'pulire il bancone'] }
Risulta abbastanza evidente che quando viene modificato l'array lavoro
nell'oggetto cloneBar
la modifica si riflette anche sull'oggetto originale. Quindi, come ottenere una clonazione profonda? Esistono diverse soluzioni come per esempio la serializzazione JSON, quest'ultima però non permette di gestire oggetti contenenti riferimenti circolari o funzioni.
Un'alternativa più efficace consiste invece nell'uso della libreria Lodash, particolarmente utile quando si tratta di manipolare oggetti, array, stringhe e altri tipi di dati in JavaScript:
const _ = require('lodash');
const barneyOriginale = { nome: 'Barney', cognome: 'Gumble' };
const barneyClonato = _.cloneDeep(barneyOriginale);
Nel codice proposto è stata utilizzata la funzione cloneDeep
fornita dalla libreria Lodash per effettuare una clonazione profonda dell'oggetto barneyOriginale
in barneyClonato
. Tutte le proprietà e le strutture dati nidificate verranno copiate separatamente. In questo modo le modifiche apportate all'oggetto clonato non influenzeranno l'originale, quindi barneyClonato
sarà una copia completamente indipendente di barneyOriginale
.
Conclusioni
La clonazione di oggetti in JavaScript non è un'operazione particolarmente complessa, bisogna però saper distinguere tra clonazione superficiale e clonazione profonda. In questa guida sono state analizzate entrambe le modalità presentando infine la clonazione con Lodash come esempio di clonazione profonda.