W3docs

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.

javascript— editable

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.

javascript— editable

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 variable

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

javascript— editable

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.

javascript— editable

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

Warning

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.

javascript— editable

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 correctly

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

It works for plain, JSON-safe data, but it has real pitfalls:

  • Functions and undefined are dropped — keys holding them simply disappear.
  • Date objects become stringsJSON.stringify turns 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.
Info

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:

javascript— editable

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.
  • const fixes the binding, not the contents — const objects 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 structuredClone over the lossy JSON.parse(JSON.stringify(...)) trick.

Test Your Knowledge

Practice
After let a = {}; let b = a; b.x = 1; — what is a.x?
After let a = {}; let b = a; b.x = 1; — what is a.x?
Practice
Given let c = { n: 1 }; let d = { n: 1 }; — what does c === d evaluate to?
Given let c = { n: 1 }; let d = { n: 1 }; — what does c === d evaluate to?
Practice
Which statement about copying objects in JavaScript is correct?
Which statement about copying objects in JavaScript is correct?
Was this page helpful?