Riferimenti e copia di object in JavaScript
Come JavaScript gestisce object per riferimento, come clonare con Object.assign e lo spread, e come eseguire una deep-clone sicura con structuredClone().
Una delle distinzioni più importanti in JavaScript riguarda il modo in cui vengono trattati i valori primitivi rispetto agli object. I primitivi vengono copiati "per intero", mentre gli object vengono archiviati e copiati "per riferimento". Fraintendere questa singola regola è la fonte di innumerevoli bug; questa guida illustra esattamente cosa accade in memoria, come confrontare e clonare gli object e come copiarli in modo sicuro.
I primitivi vengono copiati per valore
Un primitivo — una string, un numero, un boolean, null, undefined, bigint o un symbol — viene copiato come valore completo e indipendente. Assegnare una variabile a un'altra duplica il valore, quindi le due variabili risultano completamente separate.
let message = "Hello";
let phrase = message; // a full copy of the string is made
phrase = "Goodbye";
console.log(message); // "Hello" — unaffected
console.log(phrase); // "Goodbye"Modificare phrase non ha alcun effetto su message. In memoria esistono due string indipendenti.
Gli object vengono archiviati e copiati per riferimento
Gli object funzionano diversamente. Una variabile assegnata a un object non contiene l'object stesso, ma un riferimento (un puntatore) al punto in cui l'object risiede in memoria. Copiare quella variabile copia il riferimento, non l'object. Entrambe le variabili puntano quindi allo stesso object.
Esiste ancora un unico object. Abbiamo semplicemente due variabili — user e admin — che lo referenziano entrambe. Una modifica effettuata tramite l'una è visibile tramite l'altra, perché descrivono la stessa cosa.
Confronto per riferimento
Due variabili object sono uguali con == o === solo quando referenziano lo stesso object. Due object indipendenti non sono mai uguali, anche quando il loro contenuto sembra identico.
Questo è intenzionale: === applicato agli object risponde alla domanda "questi sono lo stesso object?", non "questi object hanno gli stessi dati?". Per confrontare il contenuto occorre un approccio diverso (spesso confrontando le proprietà una per una, o serializzando con JSON).
Gli object const possono ancora essere mutati
Una sorpresa comune: un object dichiarato con const può comunque avere le proprie proprietà modificate. const congela il binding — la variabile non può mai essere riassegnata a un valore diverso — ma non congela il contenuto dell'object.
const user = { name: 'John' };
user.name = 'Pete'; // OK — we are mutating the object, not reassigning the variable
console.log(user.name); // 'Pete'
// user = { name: 'Alice' }; // TypeError: Assignment to constant variableLa prima modifica funziona perché user referenzia ancora lo stesso object. La riassegnazione fallisce perché farebbe puntare user a un nuovo object, cosa che const vieta. (Per congelare anche il contenuto, usa Object.freeze().)
Clonazione superficiale
E se si volesse davvero una copia separata di un object? Bisogna creare un nuovo object e copiare le proprietà esistenti al suo interno. Due modi moderni e concisi per farlo sono Object.assign e la sintassi spread.
Object.assign(target, ...sources) copia tutte le proprietà proprie enumerabili dalle sorgenti nel target e restituisce il target:
La sintassi spread {...obj} fa la stessa cosa in forma ancora più compatta (vedi parametri rest e sintassi spread):
let user = { name: 'John', age: 30 };
let clone = { ...user }; // a shallow copy
let merged = { ...user, age: 31 }; // copy, then override age
console.log(clone); // { name: 'John', age: 30 }
console.log(merged); // { name: 'John', age: 31 }Entrambe le tecniche producono una copia reale e indipendente, ma solo al primo livello. Questa precisazione è importante e introduce la sezione successiva.
Il problema degli object annidati
Una copia superficiale duplica le proprietà di primo livello. Ma se il valore di una proprietà è a sua volta un object o un array, viene copiato solo il riferimento a quell'object annidato, non l'object annidato stesso. Il clone e l'originale condividono quindi lo stesso object annidato.
Anche se clone è un object di primo livello separato, clone.sizes e user.sizes puntano allo stesso object annidato. Mutarlo attraverso uno dei due percorsi influisce su entrambi. Questo è esattamente il tipo di bug da riferimento condiviso accidentale che colpisce chi presume che una copia con spread sia "completamente indipendente".
Spread ({...obj}) e Object.assign copiano solo un livello in profondità. Se l'object contiene object o array annidati, la copia condivide quei valori annidati con l'originale — mutare una proprietà annidata tramite uno modificherà silenziosamente anche l'altro. Per dati annidati, usa una deep clone.
Deep cloning con structuredClone()
Per copiare un object e tutto ciò che è annidato al suo interno, usa il built-in structuredClone(). Clona ricorsivamente object e array annidati, rendendo il risultato completamente indipendente dall'originale.
structuredClone gestisce anche i riferimenti circolari (un object che, direttamente o indirettamente, fa riferimento a se stesso) senza crash:
let user = {};
user.self = user; // user references itself
let clone = structuredClone(user);
console.log(clone === clone.self); // true — the cycle is preserved correctlyOltre agli object e agli array semplici, supporta molti tipi built-in, tra cui Date, Map, Set, RegExp, ArrayBuffer e gli array tipizzati — copiandone i valori fedelmente senza trasformarli in qualcosa d'altro.
Limitazioni di structuredClone
structuredClone è potente ma non universale:
- Non può clonare le funzioni — il tentativo lancia un
DataCloneError. Lo stesso vale per i nodi DOM. - Non copia le proprietà con chiavi symbol, i getter/setter delle proprietà, né la catena del prototipo dell'object — questi vengono semplicemente rimossi dal risultato.
// This throws DataCloneError because functions can't be cloned:
// structuredClone({ run: () => {} });Se devi copiare funzioni o istanze di classi con i relativi metodi, structuredClone non è lo strumento adatto — dovrai scrivere un clone personalizzato, o usare una libreria come cloneDeep di lodash.
Il vecchio trucco JSON
Prima che structuredClone fosse ampiamente disponibile, un popolare hack per la deep clone consisteva nel serializzare un object in una stringa JSON e rianalizzarla:
let user = { name: 'John', sizes: { height: 182, width: 50 } };
let clone = JSON.parse(JSON.stringify(user)); // deep copy via JSON
clone.sizes.width = 60;
console.log(user.sizes.width); // 50 — original unaffectedFunziona per dati semplici e compatibili con JSON, ma presenta problemi reali:
- Le funzioni e
undefinedvengono eliminati — le chiavi che li contengono scompaiono semplicemente. - Gli object
Datediventano string —JSON.stringifytrasforma una data in una stringa ISO e il parsing non la riconverte. Map,Sete altri tipi speciali vengono persi o diventano object vuoti.- I riferimenti circolari lanciano un
TypeError.
Per la deep cloning, preferisci structuredClone() al trucco JSON.parse(JSON.stringify(...)). L'approccio JSON perde silenziosamente funzioni, undefined e tipi speciali, altera le date e va in crash sui riferimenti circolari — structuredClone li gestisce tutti correttamente.
Una nota sul garbage collection
Una volta che si inizia a copiare riferimenti, vale la pena ricordare come JavaScript recupera la memoria. Un object rimane in memoria solo finché è raggiungibile — finché qualche variabile, proprietà o elemento di array lo referenzia. Quando l'ultimo riferimento a un object scompare, diventa irraggiungibile ed è eleggibile per il garbage collection. Non si liberano mai gli object manualmente; è il motore a farlo automaticamente.
Caso pratico: copiare lo stato prima della mutazione
Non si tratta di teoria. Nel codice UI moderno (React, Redux e simili) la regola standard è "non mutare mai lo stato direttamente — produci una nuova copia con la modifica applicata". La copia superficiale con spread è lo strumento quotidiano per questo:
Si noti che eseguiamo lo spread sia dell'object di primo livello sia dell'array annidato cart. Poiché lo spread è superficiale, è necessario copiare esplicitamente ogni livello annidato che si intende modificare — altrimenti si ricade nella trappola del riferimento condiviso descritta in precedenza. Quando i dati sono profondamente annidati e serve una copia completa e sicura, ricorri a structuredClone.
Riepilogo
- I primitivi vengono copiati per valore — le copie sono completamente indipendenti.
- Gli object vengono archiviati e copiati per riferimento — copiare la variabile copia il puntatore, quindi entrambe le variabili condividono un unico object.
===confronta gli object per riferimento: solo lo stesso object è uguale a se stesso.constfissa il binding, non il contenuto — gli objectconstpossono comunque essere mutati.Object.assign({}, obj)e{...obj}producono copie superficiali che condividono gli object annidati.structuredClone(obj)produce una copia profonda, gestisce i riferimenti circolari e molti built-in, ma non può clonare funzioni o nodi DOM e rimuove le chiavi symbol, i getter e i prototipi.- Preferisci
structuredCloneal perdenteJSON.parse(JSON.stringify(...)).