W3docs

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.

javascript— editable

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.

javascript— editable

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 variable

La 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:

javascript— editable

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.

javascript— editable

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".

Attenzione

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.

javascript— editable

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 correctly

Oltre 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 unaffected

Funziona per dati semplici e compatibili con JSON, ma presenta problemi reali:

  • Le funzioni e undefined vengono eliminati — le chiavi che li contengono scompaiono semplicemente.
  • Gli object Date diventano stringJSON.stringify trasforma una data in una stringa ISO e il parsing non la riconverte.
  • Map, Set e altri tipi speciali vengono persi o diventano object vuoti.
  • I riferimenti circolari lanciano un TypeError.
Informazione

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:

javascript— editable

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.

  • 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.
  • const fissa il binding, non il contenuto — gli object const possono 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 structuredClone al perdente JSON.parse(JSON.stringify(...)).

Metti alla prova le tue conoscenze

Pratica
Dopo let a = {}; let b = a; b.x = 1; — qual è il valore di a.x?
Dopo let a = {}; let b = a; b.x = 1; — qual è il valore di a.x?
Pratica
Dato let c = { n: 1 }; let d = { n: 1 }; — cosa restituisce c === d?
Dato let c = { n: 1 }; let d = { n: 1 }; — cosa restituisce c === d?
Pratica
Quale affermazione sulla copia di object in JavaScript è corretta?
Quale affermazione sulla copia di object in JavaScript è corretta?
Was this page helpful?