JavaScript Object References and Copying
Understand how JavaScript objects are stored and copied by reference, and how to safely clone them using Object.assign, spread, and structuredClone().
One of the most important distinctions in JavaScript is how it treats primitive values versus objects. Primitives are copied "as a whole value", but objects are stored and copied "by reference". Misunderstanding this single rule is the source of countless bugs, so this guide walks through exactly what happens in memory, how to compare and clone objects, and how to copy them safely.
Primitives Are Copied by Value
A primitive — a string, number, boolean, null, undefined, bigint, or symbol — is copied as a complete, independent value. Assigning one variable to another duplicates the value, so the two variables are entirely separate afterward.
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"Changing phrase has no effect on message. There are two independent strings in memory.
Objects Are Stored and Copied by Reference
Objects work differently. A variable assigned to an object does not hold the object itself — it holds a reference (a pointer) to where the object lives in memory. Copying that variable copies the reference, not the object. Both variables then point to the same object.
There is still only one object. We just have two variables — user and admin — that both reference it. A change made through either one is visible through the other, because they describe the same thing.
Comparison by Reference
Two object variables are equal with == or === only when they reference the same object. Two independent objects are never equal, even when their contents look identical.
This is intentional: === for objects asks "are these the same object?", not "do these objects have the same data?". Comparing contents requires a different approach (often comparing properties one by one, or serializing with JSON).
const Objects Can Still Be Mutated
A common surprise: an object declared with const can still have its properties changed. const freezes the binding — the variable can never be reassigned to a different value — but it does not freeze the object's contents.
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 variableThe first change works because user still references the same object. The reassignment fails because it would point user at a brand-new object, which const forbids. (To freeze the contents too, use Object.freeze().)
Shallow Cloning
What if you genuinely want a separate copy of an object? You have to create a new object and copy the existing properties into it. Two modern, concise ways to do this are Object.assign and the spread syntax.
Object.assign(target, ...sources) copies all enumerable own properties from the sources into the target and returns the target:
The spread syntax {...obj} does the same thing in an even shorter form (see rest parameters and spread syntax):
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 }Both techniques produce a real, independent copy — but only at the top level. That qualifier matters, and it leads to the next section.
The Nested-Object Problem
A shallow copy duplicates the top-level properties. But if a property's value is itself an object or array, only the reference to that nested object is copied — not the nested object itself. The clone and the original therefore share the same nested object.
Even though clone is a separate top-level object, clone.sizes and user.sizes point to one and the same nested object. Mutating it through either path affects both. This is exactly the kind of accidental shared-reference bug that bites people who assume a spread copy is "fully independent".
Spread ({...obj}) and Object.assign only copy one level deep. If your object contains nested objects or arrays, the copy shares those nested values with the original — mutating a nested property through one will silently change the other. For nested data, use a deep clone.
Deep Cloning with structuredClone()
To copy an object and everything nested inside it, use the built-in structuredClone(). It recursively clones nested objects and arrays, so the result is fully independent from the original.
structuredClone also handles circular references (an object that, directly or indirectly, refers back to itself) without crashing:
let user = {};
user.self = user; // user references itself
let clone = structuredClone(user);
console.log(clone === clone.self); // true — the cycle is preserved correctlyBeyond plain objects and arrays, it supports many built-in types, including Date, Map, Set, RegExp, ArrayBuffer, and typed arrays — copying their values faithfully rather than turning them into something else.
Limitations of structuredClone
structuredClone is powerful but not universal:
- It cannot clone functions — attempting to do so throws a
DataCloneError. The same applies to DOM nodes. - It does not copy symbol-keyed properties, property getters/setters, or the object's prototype chain — those are simply stripped from the result.
// This throws DataCloneError because functions can't be cloned:
// structuredClone({ run: () => {} });If you need to copy functions or class instances with their methods, structuredClone isn't the tool — you'd write a custom clone, or use a library such as lodash's cloneDeep.
The Old JSON Trick
Before structuredClone was widely available, a popular deep-clone hack was to serialize an object to a JSON string and parse it back:
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 unaffectedIt works for plain, JSON-safe data, but it has real pitfalls:
- Functions and
undefinedare dropped — keys holding them simply disappear. Dateobjects become strings —JSON.stringifyturns a date into an ISO string, and parsing does not turn it back.Map,Set, and other special types are lost or become empty objects.- Circular references throw a
TypeError.
For deep cloning, prefer structuredClone() over the JSON.parse(JSON.stringify(...)) trick. The JSON approach silently loses functions, undefined, and special types, mangles dates, and crashes on circular references — structuredClone handles all of those correctly.
A Note on Garbage Collection
Once you start copying references around, it's worth remembering how JavaScript reclaims memory. An object stays in memory only while it is reachable — while some variable, property, or array entry still references it. When the last reference to an object disappears, it becomes unreachable and is eligible for garbage collection. You never free objects manually; the engine does it automatically.
Real-World: Copying State Before Mutation
This is not academic. In modern UI code (React, Redux, and similar) the standard rule is "never mutate state directly — produce a new copy with the change applied". Shallow copying with spread is the everyday tool for this:
Notice that we spread both the top-level object and the nested cart array. Because spread is shallow, you must explicitly copy each nested level you intend to change — otherwise you fall straight back into the shared-reference trap described earlier. When the data is deeply nested and you need a complete, safe copy, reach for structuredClone.
Summary
- Primitives are copied by value — copies are fully independent.
- Objects are stored and copied by reference — copying the variable copies the pointer, so both variables share one object.
===compares objects by reference: only the same object is equal to itself.constfixes the binding, not the contents —constobjects can still be mutated.Object.assign({}, obj)and{...obj}make shallow copies that share nested objects.structuredClone(obj)makes a deep copy, handles circular references and many built-ins, but cannot clone functions or DOM nodes and strips symbol keys, getters, and prototypes.- Prefer
structuredCloneover the lossyJSON.parse(JSON.stringify(...))trick.