Skip to main content

How to Understand Object References and Copying in JavaScript

When you start working with objects in JavaScript, one of the most confusing and bug-producing topics is how objects are stored, assigned, and copied. Unlike primitive values, objects behave very differently when you assign them to variables, pass them to functions, or try to duplicate them. Misunderstanding this behavior leads to some of the most common and hardest-to-debug issues in JavaScript.

This guide walks you through the memory model behind primitives and objects, explains why two variables can point to the same object, and shows you every technique available for copying objects, from shallow to deep.

Primitive vs. Reference Types: The Memory Model

JavaScript stores primitive values and objects in fundamentally different ways. Understanding this difference is the key to everything that follows.

How Primitives Are Stored

When you assign a primitive value (string, number, boolean, null, undefined, symbol, bigint) to a variable, the actual value is stored directly in the variable's memory slot. When you copy it to another variable, a completely independent copy of that value is created.

let name = "Alice";
let copy = name;

copy = "Bob";

console.log(name); // "Alice" (untouched)
console.log(copy); // "Bob"

Each variable holds its own independent value. Changing one has absolutely no effect on the other.

How Objects Are Stored

Objects work differently. When you create an object, JavaScript stores the object somewhere in memory (the heap), and the variable receives a reference (think of it as an address or a pointer) to that location, not the object itself.

let user = { name: "Alice", age: 25 };

Here, user does not "contain" the object. It contains a reference that points to where { name: "Alice", age: 25 } lives in memory.

You can visualize it like this:

Variable         Memory (Heap)
-------- -------------
user ------> { name: "Alice", age: 25 }
Think of It This Way

A primitive variable is like a sticky note with a value written on it. An object variable is like a sticky note with an address written on it. The address points to a house (the object). Multiple sticky notes can have the same address.

Reference Assignment: Two Variables, One Object

When you assign an object variable to another variable, you are copying the reference, not the object. Both variables now point to the exact same object in memory.

let user = { name: "Alice", age: 25 };
let admin = user;

console.log(admin.name); // "Alice"

Both user and admin point to the same object:

Variable         Memory (Heap)
-------- -------------
user ------> { name: "Alice", age: 25 }
admin ----/

This means that modifying the object through one variable is immediately visible through the other:

let user = { name: "Alice", age: 25 };
let admin = user;

admin.name = "Bob";

console.log(user.name); // "Bob" (changed through admin, visible through user)

This is not a bug. Both variables reference the same single object. There is only one object in memory, and two ways to access it.

Function Arguments Work the Same Way

When you pass an object to a function, the function receives a copy of the reference, not a copy of the object. This means the function can modify the original object:

function updateAge(obj) {
obj.age = 30;
}

let user = { name: "Alice", age: 25 };
updateAge(user);

console.log(user.age); // 30 (the original object was modified)
caution

This is one of the most common sources of unexpected bugs. If you pass an object to a function and the function modifies it, the caller's object changes too.

Comparison by Reference vs. by Value

Understanding how JavaScript compares primitives and objects is critical.

Primitives Are Compared by Value

let a = "hello";
let b = "hello";

console.log(a === b); // true (same value)

Objects Are Compared by Reference

Two objects are equal only if they are the exact same object in memory (same reference). Two different objects with identical contents are not equal.

let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };

console.log(obj1 === obj2); // false (different objects in memory)
console.log(obj1 == obj2); // false (still different references)

Even though obj1 and obj2 have identical properties, they are two separate objects stored at different memory locations.

However, if two variables point to the same object:

let obj1 = { name: "Alice" };
let obj2 = obj1;

console.log(obj1 === obj2); // true (same reference)
note

There is no built-in operator in JavaScript that compares objects by their contents. You need to write your own deep comparison function or use a library.

Shallow Copy: Object.assign() and the Spread Operator

When you need an independent copy of an object so that changes to one don't affect the other, you need to clone it. The simplest approach is a shallow copy.

A shallow copy creates a new object and copies all top-level properties from the original. If a property's value is a primitive, the value itself is copied. If a property's value is an object (nested object, array, etc.), only the reference is copied.

Using Object.assign()

Object.assign(target, ...sources) copies all enumerable own properties from one or more source objects into a target object.

let user = { name: "Alice", age: 25 };

let clone = Object.assign({}, user);

clone.name = "Bob";

console.log(user.name); // "Alice" (original is safe)
console.log(clone.name); // "Bob"

You can also merge multiple objects:

let defaults = { theme: "light", lang: "en" };
let userPrefs = { theme: "dark" };

let settings = Object.assign({}, defaults, userPrefs);

console.log(settings); // { theme: "dark", lang: "en" }

Using the Spread Operator {...obj}

The spread syntax is the modern, more concise way to create shallow copies:

let user = { name: "Alice", age: 25 };

let clone = { ...user };

clone.name = "Bob";

console.log(user.name); // "Alice"
console.log(clone.name); // "Bob"

You can also add or override properties while spreading:

let user = { name: "Alice", age: 25 };

let updated = { ...user, age: 30, role: "admin" };

console.log(updated); // { name: "Alice", age: 30, role: "admin" }

Shallow Copy for Arrays

The same concept applies to arrays. Arrays are objects, and the spread operator or Array.from() creates a shallow copy:

let original = [1, 2, 3];
let copy = [...original];

copy.push(4);

console.log(original); // [1, 2, 3] (untouched)
console.log(copy); // [1, 2, 3, 4]

The Limitation of Shallow Copy

Shallow copies only go one level deep. If your object has nested objects, the nested references are shared between the original and the copy. This is where most bugs occur, and we will address it in detail below.

Deep Copy: structuredClone(), JSON Method, and Limitations

When your object contains nested objects, arrays, or other complex structures, you need a deep copy, a clone where every level of nesting is independently duplicated.

Introduced as a globally available function, structuredClone() is the modern, built-in way to perform deep cloning in JavaScript.

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
},
hobbies: ["reading", "coding"]
};

let clone = structuredClone(user);

clone.address.city = "Milan";
clone.hobbies.push("gaming");

console.log(user.address.city); // "Rome" (original untouched)
console.log(user.hobbies); // ["reading", "coding"] (original untouched)
console.log(clone.address.city); // "Milan"
console.log(clone.hobbies); // ["reading", "coding", "gaming"]

structuredClone() handles:

  • Nested objects and arrays
  • Date objects
  • Map, Set
  • ArrayBuffer, Blob, File, ImageData
  • RegExp
  • Circular references (objects that reference themselves)
let obj = {};
obj.self = obj; // circular reference

let clone = structuredClone(obj);
console.log(clone.self === clone); // true (circular ref preserved correctly)

What structuredClone() cannot clone:

  • Functions
  • DOM elements
  • Symbol properties
  • Prototype chain (the clone will be a plain object)
  • Error objects (limited support)
let obj = {
greet: function() { return "hello"; }
};

// This will throw a DataCloneError
structuredClone(obj);
tip

structuredClone() is available in all modern browsers and Node.js 17+. For most use cases, it is the best choice for deep cloning.

The JSON Method: JSON.parse(JSON.stringify())

Before structuredClone() existed, the most common deep cloning trick was the JSON round-trip:

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
}
};

let clone = JSON.parse(JSON.stringify(user));

clone.address.city = "Milan";

console.log(user.address.city); // "Rome"
console.log(clone.address.city); // "Milan"

This works by converting the object to a JSON string and then parsing it back into a new object. It creates a fully independent deep copy.

However, the JSON method has serious limitations:

What Gets LostReason
undefined valuesNot valid in JSON
FunctionsNot valid in JSON
Symbol propertiesNot valid in JSON
Date objectsConverted to strings, not restored as Date
Map, SetConverted to {}
RegExpConverted to {}
Infinity, NaNConverted to null
Circular referencesThrows an error
let obj = {
date: new Date("2024-01-01"),
regex: /hello/gi,
fn: function() {},
undef: undefined,
nan: NaN,
set: new Set([1, 2, 3])
};

let clone = JSON.parse(JSON.stringify(obj));

console.log(clone);
// {
// date: "2024-01-01T00:00:00.000Z", ← string, not Date
// regex: {}, ← empty object
// nan: null, ← null, not NaN
// set: {} ← empty object
// // fn and undef are gone entirely
// }
warning

The JSON method silently drops or transforms data. Use it only when you are certain your object contains only JSON-safe types (string, number, boolean, null, plain objects, and arrays).

structuredClone() vs. JSON: Quick Comparison

FeaturestructuredClone()JSON.parse(JSON.stringify())
Nested objects/arraysYesYes
Date objectsYes (preserved)No (becomes string)
Map, SetYesNo
Circular referencesYesThrows error
FunctionsNoNo
undefinedYesNo (dropped)
NaN, InfinityYesNo (becomes null)
PerformanceGenerally fasterSlower for large objects

Libraries for Deep Cloning

For complex scenarios where you need maximum flexibility, third-party libraries offer robust deep cloning utilities.

Lodash _.cloneDeep

Lodash's _.cloneDeep is one of the most widely used deep cloning utilities:

import _ from "lodash";

let user = {
name: "Alice",
address: {
city: "Rome",
coords: [41.9, 12.5]
}
};

let clone = _.cloneDeep(user);

clone.address.city = "Milan";
console.log(user.address.city); // "Rome"

You can also import only cloneDeep to reduce bundle size:

import cloneDeep from "lodash/cloneDeep";

let clone = cloneDeep(user);

When to Use a Library

  • You need to clone objects with functions, custom class instances, or complex prototype chains
  • You work in an environment without structuredClone() (older Node.js versions)
  • You need custom cloning behavior (Lodash's _.cloneDeepWith allows a customizer function)
Decision Guide

For most projects, prefer structuredClone(). Reach for Lodash only when structuredClone() does not meet your requirements, such as cloning objects with functions or custom prototypes.

Common Mistake: Shallow Copy of Nested Objects

This is the single most frequent mistake developers make with object copying. Let's see it in action and understand exactly what goes wrong.

The Wrong Way

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
}
};

// Shallow copy using spread
let clone = { ...user };

// Modify nested property
clone.address.city = "Milan";

console.log(user.address.city); // "Milan" (OOPS! Original changed!)

Why does this happen?

The spread operator copies properties one level deep. The name property is a string (primitive), so it gets a truly independent copy. But address is an object, so only the reference is copied. Both user.address and clone.address point to the same object in memory.

user.address  ------>  { city: "Milan", zip: "00100" }
clone.address ----/

This exact same problem happens with Object.assign():

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
}
};

let clone = Object.assign({}, user);
clone.address.city = "Milan";

console.log(user.address.city); // "Milan" (same problem)

And with arrays containing objects:

let users = [
{ name: "Alice" },
{ name: "Bob" }
];

let copy = [...users];

copy[0].name = "Charlie";

console.log(users[0].name); // "Charlie" (original array's object was modified!)

The Correct Way

Option 1: Use structuredClone() (best for most cases)

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
}
};

let clone = structuredClone(user);

clone.address.city = "Milan";

console.log(user.address.city); // "Rome" (safe!)
console.log(clone.address.city); // "Milan"

Option 2: Manual nested spread (for simple, known structures)

let user = {
name: "Alice",
address: {
city: "Rome",
zip: "00100"
}
};

let clone = {
...user,
address: { ...user.address }
};

clone.address.city = "Milan";

console.log(user.address.city); // "Rome" (safe!)
console.log(clone.address.city); // "Milan"

This manual approach works but becomes impractical for deeply nested objects. You would need to spread every nested level manually, and if the structure changes, you must update the cloning code.

Option 3: JSON round-trip (if your data is JSON-safe)

let clone = JSON.parse(JSON.stringify(user));

Option 4: Lodash _.cloneDeep (for complex scenarios)

import cloneDeep from "lodash/cloneDeep";
let clone = cloneDeep(user);

How to Spot This Bug

Watch out for these patterns in your code:

// Any of these create only a shallow copy:
let copy = { ...original };
let copy = Object.assign({}, original);
let copy = [...originalArray];

// If the original has nested objects/arrays,
// modifying nested properties on the copy WILL affect the original.
Rule of Thumb

If your object has any nesting (objects inside objects, arrays of objects, etc.), a shallow copy is not enough. Use structuredClone() or a library for a true independent clone.

Quick Reference: Choosing the Right Copy Method

ScenarioRecommended Method
Flat object (no nesting){ ...obj } or Object.assign({}, obj)
Nested object, JSON-safe datastructuredClone()
Object with Date, Map, SetstructuredClone()
Object with circular referencesstructuredClone()
Object with functions or class instances_.cloneDeep() from Lodash
Quick and dirty (JSON-safe only)JSON.parse(JSON.stringify(obj))
Simple array of primitives[...arr] or Array.from(arr)
Array of objectsstructuredClone(arr)

Understanding how references and copying work in JavaScript is fundamental to writing predictable, bug-free code. The key takeaway is simple: primitives are copied by value, objects are copied by reference. Whenever you need a true independent copy of an object, be intentional about whether a shallow or deep copy is appropriate, and choose the right tool for the job.