Shallow copy e Deep copy - come clonare un oggetto

Aggiornato il 20/6/2022

Le variabili in javascript

In javascript esistono 7 tipi di variabili chiamate ‘primitive’. Le stringhe, i numeri e i valori booleani, per esempio, sono delle primitive. Gli oggetti (Object) invece, sono speciali e vengono trattati in modo diverso.

Quando creiamo un oggetto (let obj = {}), la variabile che stiamo creando non conterrà l’oggetto effettivo, ma solo un puntatore che fa riferimento all’indirizzo di memoria dove possiamo trovare l’oggetto. Tutto ciò viene fatto in maniera totalmente trasparente, e quindi potrebbe passare inosservato. Quando però proviamo a copiare un oggetto, potremmo avere dei problemi. Prendiamo in considerazione questo codice:

let oggetto = { nome: "Mario" };

let copia = oggetto;

Ci verrebbe da pensare che la seconda variabile, essendo una copia, sia totalmente separata e possa essere usata senza preoccuparsi troppo. Invece con questo codice noi abbiamo copiato solo il riferimento all’oggetto, non l’oggetto vero e proprio! Per questo motivo, se facciamo una modifica ad una di queste variabili, vedremo che entrambe cambieranno:

copia.nome = "Luca";

console.log(copia); // { nome: 'Luca' }

console.log(oggetto); // { nome: 'Luca' } !!!

Questo effetto particolare lo troviamo per tutti quei costrutti che utilizziamo nel nostro codice e che derivano dal tipo ‘Object’, come gli Array, i Map, e molto altro.

Come facciamo quindi a copiare questi oggetti? Vediamo di seguito due metodi.

Copia superficiale

La copia superficiale (Shallow Copy) ti permette di copiare tutti i valori contenuti in un oggetto o un Array in una nuova variabile. Non essendo una copia per indirizzo, possiamo modificare questa nuova variabile senza preoccuparci di creare qualche strano effetto collaterale. In javascript esistono diversi metodi per copiare un oggetto in modo superficiale. Il metodo più semplice è quello di usare la sintassi spread, quando si tratta di copiare un Array o un Oggetto:

let oggetto = { nome: "Mario" };

let copia = { ...oggetto }; // copia

copia.nome = "Luca";

console.log(copia); // { nome: 'Luca' }

console.log(oggetto); // { nome: 'Mario' }

Un altro metodo è quello di usare la funzione Object.assign(...) (Documentazione MDN):

let oggetto = { nome: "Mario" };

let copia = Object.assign({}, oggetto);

Il risultato è praticamente lo stesso della sintassi spread, solo che in questo caso vengono invocate le funzioni getter e setter delle proprietà dell’oggetto (se sono presenti).

Limitazioni

Anche se l’oggetto o l’Array di per sé viene copiato correttamente, i dati contenuti sono copiati per indirizzo, spostando in effetti il problema dall’oggetto alle sue proprietà:

let utente = {
  nome: "Mario",
  lavoro: {
    orario: "part-time",
    incarico: "impiegato",
  },
};

let copia = { ...utente }; // Otteniamo lo stesso risultato con Object.assign

utente.nome = "Luca";
utente.lavoro.orario = "full-time";

console.log(copia.nome); // 'Mario' - essendo una primitiva, il valore viene copiato correttamente

console.log(copia.lavoro.orario); // 'full-time' - La proprietà 'lavoro' viene copiata per indirizzo

Per questo motivo, in certe circostanze può essere necessario creare una copia profonda per evitare effetti collaterali.

Comunque, nella maggior parte delle situazioni, una shallow copy è più che sufficiente ed è migliore in quanto a velocità.

Copia profonda

Aggiornamento dal Google I/O 2022

Ora è disponibile una nuova funzione chiamata structuredClone che è in grado di creare copie profonde di oggetti anche molto complessi. Recentemente, questa funzione è diventata disponibile in tutti i maggiori browser web, oltre che su node e deno 🥳

Ecco in breve come funziona:

let utente = {
    nome: "Mario",
    lavoro: {
        orario: "part-time"
    }
};

let copia = structuredClone(utente);

// Questo piccolo test ci indica se è una copia profonda
copia.lavoro.orario = "full-time";

console.log(utente.lavoro.orario); // --> "part-time"

Implementare la copia profonda (Deep Copy) non è per niente semplice. Javascript è un linguaggio molto elastico e ci sono parecchie situazioni che possono creare dei problemi.

Il metodo più semplice e veloce per creare una copia profonda è il seguente:

let copia = JSON.parse(JSON.stringify(oggetto));

Con questo codice andiamo a convertire l’oggetto originale in una stringa JSON e poi lo ritrasformiamo in oggetto, creando una copia profonda. In questo modo non dovremo più preoccuparci che la nostra variabile punti all’indirizzo sbagliato.

Questo metodo, però, ha una forte limitazione: l’oggetto deve poter essere convertito in JSON. Purtroppo molti tipi di oggetti non sono convertibili (Map, Set, Blob, FileList e in pratica tutti i tipi particolari che derivano da ‘Object’). Inoltre, per lo stesso motivo, potremmo perdere diverse informazioni legate all’oggetto, ma che non sono sue proprietà (descriptor, attributi dei prototipi, …).

Conoscendo il problema, alcune famose librerie includono una funzione per effettuare una copia profonda. Se stai già usando una libreria, controlla quindi se fornisce una funzione per questa circostanza, altrimenti ecco un paio di esempi di questa funzione in due librerie molto utilizzate (lodash e angularjs):

// lodash
let copia = _.cloneDeep(oggetto);

// angularjs
let copia = angular.copy(oggetto);

Comprendere bene il funzionamento di javascript anche nelle cose così apparentemente semplici vi sarà sicuramente d’aiuto, dato che questa particolarità causa spesso degli errori difficili da controllare.

Buon lavoro!