Deep Copy vs Shallow Copy

Reviewed & published by Brayan K

A deep copy duplicates an object along with all the nested objects it contains, creating a fully independent clone, whereas a shallow copy only copies the top level and still shares references to nested values.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

Master memory management, reference handling, and data structure copying

What You'll Learn in This Lesson

Understanding Reference vs Value

Understanding how JavaScript handles data structures, memory, and copying is one of the most important skills for writing bug-free, scalable code. Many of the hardest-to-diagnose bugs come directly from not knowing whether you created a real copy or a shared reference.

JavaScript stores values in two completely different ways: by value and by reference. Primitives such as numbers, strings, booleans, null, undefined, symbol, and bigint are stored by value, meaning each assignment creates a brand-new, independent copy. Objects, arrays, functions, Maps, Sets, and Dates are stored by reference, meaning multiple variables can point to the same memory.

// Primitives are stored by value
let x = 10;
let y = x;
y = 20;

console.log(x); // 10 – primitive types are fully independent

// Objects are stored by reference
let a = { score: 10 };
let b = a;

b.score = 99;
console.log(a.score); // 99 – both point to the same reference

// This is why you can break entire applications by accidentally 
// modifying the original object when you thought you were modifying a copy

💡 Key Insight: Understanding this distinction lets you debug faster, structure data correctly, and build cleaner architecture.

Shallow Copying: The Hidden Trap

A shallow copy duplicates only the top level of an object. It does not clone nested structures. JavaScript provides several shallow-cloning techniques that developers often assume are deep copies, which leads to subtle bugs.

const user = { name: "Leo", prefs: { theme: "dark" } };
const copy = { ...user };

copy.prefs.theme = "light";

console.log(user.prefs.theme); 
// "light" – shallow copies share nested objects

// Common shallow copy operations:
// • Spread operator { ...obj }
// • Object.assign({}, obj)
// • Array methods like .slice(), .concat()

const items = [
  { id: 1, qty: 1 },
  { id: 2, qty: 3 }
];

const shallow = items.slice();
shallow[0].qty = 99;

console.log(items[0].qty); 
// 99 – mutation leaked through shallow copy

⚠️ Warning: These operations are fast and convenient, but unsafe when dealing with nested structures.

Deep Copying: True Independence

A deep copy creates a fully independent clone, duplicating every level of nesting so changes never leak back into the original structure.

Why deep copies matter:

// Modern JavaScript provides structuredClone()
const user = { name: "Leo", prefs: { theme: "dark" } };
const deep = structuredClone(user);

deep.prefs.theme = "light";
console.log(user.prefs.theme); 
// "dark" – deep clone protects the original

// Nested arrays example
const cart = [{ item: "Book", qty: 1 }];
const cloned = structuredClone(cart);

cloned[0].qty = 5;
console.log(cart[0].qty); // 1 – no mutation

// structuredClone() handles:
// • Dates, RegExp, Map, Set, ArrayBuffer
// • Nested objects and arrays
// • Circular references (JSON cloning throws on these)
//
// What it does NOT do:
// • It does NOT keep prototypes. Clone a class instance and you get a plain
//   object back: the data survives, the methods and "instanceof" do not.
// • It throws DataCloneError on functions, DOM nodes and Symbols.
class Dog {
  constructor(name) { this.name = name; }
  speak() { return this.name + " says woof"; }
}

const rex = new Dog("Rex");
const rexClone = structuredClone(rex);

console.log(rexClone.name);                 // Rex — the data came across
console.log(rexClone instanceof Dog);       // false — the prototype did not
console.log(typeof rexClone.speak);         // undefined — the method is gone

// ✅ Expected output:
// dark
// 1
// Rex
// false
// undefined

Worked Example: Alias vs Shallow vs Deep, Side by Side

Read this one line by line before you write any copying code of your own. It makes all three kinds of "copy" from the same nested object, then proves which ones share the inner object using === — a comparison that asks "are these literally the same object in memory?" rather than "do they look alike". Every non-obvious line says what it prints.

// Three ways to "copy" the same nested object — and what each really does.
// The object holds a nested "prefs" object and a nested "tags" array.
// Watch which copies end up sharing them.

const original = { name: "Leo", prefs: { theme: "dark" }, tags: ["js"] };

// 1) NOT A COPY — an alias. Both names point at the very same object.
const alias = original;

// 2) SHALLOW COPY — a new outer object, the SAME inner objects.
//    Spread copies each top-level value. The value stored under "prefs" is a
//    reference, so the reference is what gets copied, not the object behind it.
const shallow = { ...original };

// 3) DEEP COPY — a brand new object, all the way down.
const deep = structuredClone(original);

// === is an identity test: "is this the exact same object in memory?"
console.log("alias shares prefs?  ", alias.prefs === original.prefs);     // true
console.log("shallow shares prefs?", shallow.prefs === original.prefs);   // true  <-- the trap
console.log("deep shares prefs?   ", deep.prefs === original.prefs);      // false

// Now edit the nested value through each copy and see what leaks back.
shallow.prefs.theme = "light";
console.log("after shallow edit, original theme:", original.prefs.theme);   // light — leaked

deep.prefs.theme = "solarized";
console.log("after deep edit, original theme:   ", original.prefs.theme);   // still light — no leak

// Top-level changes never leak from a shallow copy: the outer object IS new.
shallow.name = "Robin";
console.log("original name:", original.name);   // Leo

// The same rule applies to nested arrays.
shallow.tags.push("ts");
console.log("original tags:", original.tags.join(","));   // js,ts — shared array
console.log("deep tags:    ", deep.tags.join(","));       // js    — its own array

// ✅ Expected output:
// alias shares prefs?   true
// shallow shares prefs? true
// deep shares prefs?    false
// after shallow edit, original theme: light
// after deep edit, original theme:    light
// original name: Leo
// original tags: js,ts
// deep tags:     js

💡 The one-line rule: a shallow copy protects the top level and nothing else. If your object has any nesting at all, that is not protection.

🎯 Your Turn: Make This Update Safe

Your turn. The function below is meant to return a new settings object with a different theme while leaving the original alone — the exact job a React or Redux state update does. Three pieces are missing. Fill in each ___ and compare against the expected output at the bottom.

// 🎯 YOUR TURN — return a new settings object, leave the original untouched
// Replace each ___ then press Run.

const settings = {
  account: { user: "sam", plan: "pro" },
  display: { theme: "dark", fontSize: 14 }
};

function updateTheme(state, newTheme) {
  // 1) Take a full, independent copy FIRST.
  //    A spread here would not be enough — "display" is nested.
  const next = ___(state);        // 👉 the built-in that deep-clones an object

  // 2) Change the copy, never the original.
  next.display.theme = ___;       // 👉 which value did the caller pass in?

  // 3) Hand the copy back.
  return ___;                     // 👉 which object should the caller receive?
}

const updated = updateTheme(settings, "light");

console.log("original theme:", settings.display.theme);
console.log("updated theme:", updated.display.theme);
console.log("same object?", updated === settings);
console.log("shared display?", updated.display === settings.display);

// ✅ Expected output:
// original theme: dark
// updated theme: light
// same object? false
// shared display? false

The JSON Cloning Trap

Many developers use JSON cloning, but it's risky because it destroys important data:

const user = {
  name: "Alex",
  joined: new Date(),
  greet: () => console.log("Hello"),
  score: Infinity,
  extra: undefined
};

// JSON cloning destroys data
const cloned = JSON.parse(JSON.stringify(user));

console.log(cloned);
// {
//   name: "Alex",
//   joined: "2024-01-15T10:30:00.000Z", // Date → string
//   // greet: removed (functions lost)
//   score: null, // Infinity → null
//   // extra: removed (undefined lost)
// }

// This makes JSON cloning unsuitable for anything beyond simple data
// Circular references also break the program entirely

❌ Avoid: JSON cloning destroys Dates, Functions, Infinity/NaN, Undefined, and Circular references.

Building a Recursive Deep Clone

If you want full control, building a recursive deep clone function is the most reliable approach for understanding how cloning actually works under the hood.

function deepClone(value) {
  // Handle primitives and null
  if (value === null || typeof value !== "object") {
    return value;
  }

  // Handle arrays
  if (Array.isArray(value)) {
    return value.map(item => deepClone(item));
  }

  // Handle objects
  const clone = {};
  for (let key in value) {
    if (value.hasOwnProperty(key)) {
      clone[key] = deepClone(value[key]);
    }
  }
  return clone;
}

// Test it
const original = { 
  x: 1, 
  y: { z: 2 },
  arr: [{ id: 1 }, { id: 2 }]
};

const copy = deepClone(original);
copy.y.z = 999;
copy.arr[0].id = 777;

console.log(original.y.z); // 2 – original unchanged
console.log(original.arr[0].id); // 1 – original unchanged

// This teaches a powerful idea: if you want true independence 
// in your data, you must recursively duplicate nested references

Real-World API Example

Consider this common real-world situation: You fetch user data from an API and want to modify a piece of it for display without altering the original payload.

// ❌ WRONG: Shallow copy causes silent mutation
const apiData = {
  user: {
    name: "Eli",
    stats: { posts: 20, likes: 88 }
  }
};

const displayData = { ...apiData };
displayData.user.stats.likes = 200;

console.log(apiData.user.stats.likes); 
// 200 – silently mutated original data!

// ✅ CORRECT: Deep copy protects original
const apiData2 = {
  user: {
    name: "Eli",
    stats: { posts: 20, likes: 88 }
  }
};

const clonedData = structuredClone(apiData2);
clonedData.user.stats.likes = 200;

console.log(apiData2.user.stats.likes);
// 88 – original is protected

// This pattern is crucial when working with:
// • React state management
// • Redux stores
// • API response transformations
// • Form data manipulation
// • Undo/redo systems

Working with Maps and Sets

Data structures like Maps and Sets need similar care. Developers often assume copying a Map protects data, but it only copies the key/value references.

// ❌ Shallow copy of Map still shares nested objects
const map = new Map();
map.set("profile", { age: 18 });

const copy = new Map(map);
copy.get("profile").age = 40;

console.log(map.get("profile").age); 
// 40 – Map shallow copied, nested object shared

// ✅ Deep cloning with structuredClone
const map2 = new Map();
map2.set("profile", { age: 18 });

const deepMap = structuredClone(map2);
deepMap.get("profile").age = 40;

console.log(map2.get("profile").age);
// 18 – original safe

// Same pattern applies to Sets
const set = new Set([{ id: 1 }, { id: 2 }]);
const clonedSet = structuredClone(set);

// Modify cloned set without affecting original
const firstItem = [...clonedSet][0];
firstItem.id = 999;

console.log([...set][0].id); // 1 – original unchanged

Immutable Update Pattern

A powerful real-world pattern is immutable updates, where instead of editing data in place, you clone, change the clone, and return it. This avoids mutation chains and keeps your application predictable.

// Immutable update pattern
function updateScore(state, newScore) {
  const cloned = structuredClone(state);
  cloned.user.score = newScore;
  return cloned;
}

const initialState = {
  user: { name: "Sam", score: 100 },
  settings: { theme: "dark" }
};

const updatedState = updateScore(initialState, 250);

console.log(initialState.user.score); // 100 – unchanged
console.log(updatedState.user.score); // 250 – new state

// This pattern is used heavily in:
// • React state updates
// • Redux reducers
// • Vue/Svelte reactive stores
// • Any modern UI architecture

// It prevents mutation bugs and makes state changes predictable
function addToCart(cart, item) {
  const newCart = structuredClone(cart);
  newCart.items.push(item);
  newCart.total += item.price;
  return newCart;
}

// Original cart never mutates
const cart = { items: [], total: 0 };
const cart2 = addToCart(cart, { name: "Book", price: 20 });

console.log(cart.items.length); // 0
console.log(cart2.items.length); // 1

🎯 Pro Pattern: Immutable updates make state changes predictable and prevent cascading mutations across your app.

Performance Considerations

Reference sharing affects performance. Shallow copies are extremely fast because they only duplicate top-level references. Deep copies recursively duplicate all nested data, meaning the cost grows with data complexity.

When to use shallow copying:

When to use deep copying:

// Shallow copy: Fast but risky for nested data
const shallow = { ...bigObject }; // O(n) for top level only

// Deep copy: Slower but safe for nested data
const deep = structuredClone(bigObject); // O(n * depth)

// Choose based on data structure
const flatData = { x: 1, y: 2, z: 3 };
const shallowOK = { ...flatData }; // Safe, fast

const nestedData = {
  user: { profile: { settings: { theme: "dark" } } }
};
const deepRequired = structuredClone(nestedData); // Necessary

// Highly nested objects → slower deep copies
// Flat, normalized structures → faster operations and easier debugging
// Arrays of primitives → safe shallow copies
// Arrays of objects → require deep copying to avoid mutation leaks

Common Mistakes to Avoid

Understanding these common pitfalls will save you hours of debugging:

// ❌ MISTAKE 1: Partial deep copying
const config = {
  theme: { mode: "dark" },
  languages: ["js", "python"]
};

const clone = { 
  ...config, 
  languages: [...config.languages] // deep copy array
};

clone.theme.mode = "light"; 
// Still mutates original — theme was shallow copied!

// ✅ FIX: Deep copy everything consistently
const fixed = structuredClone(config);
fixed.theme.mode = "light";
console.log(config.theme.mode); // "dark" ✓

// ❌ MISTAKE 2: Assuming slice() is safe for nested arrays
const items = [{ id: 1, qty: 1 }];
const cloned = items.slice();
cloned[0].qty = 99;
console.log(items[0].qty); // 99 – mutation leaked

// ✅ FIX: Deep clone nested structures
const fixed2 = structuredClone(items);
fixed2[0].qty = 99;
console.log(items[0].qty); // 1 ✓

// ❌ MISTAKE 3: Mutating an array the caller still owns
function addItem(item, list) {
  list.push(item);   // this edits the CALLER'S array, not a copy of it
  return list;
}

const myList = ["a"];
const returned = addItem("b", myList);

console.log(myList.join(","));     // a,b  – the caller's array changed
console.log(returned === myList);  // true – you handed back the same array

// ✅ FIX: copy first, then change the copy
function addItemSafe(item, list = []) {
  const safeList = structuredClone(list);
  safeList.push(item);
  return safeList;
}

const before = ["a"];
const after = addItemSafe("b", before);

console.log(before.join(","));   // a    – untouched
console.log(after.join(","));    // a,b  – a brand new array

// Note if you are coming from Python: JavaScript default parameters are NOT
// shared between calls. "list = []" builds a fresh array on every single call.
console.log(addItemSafe("a").join(","));   // a
console.log(addItemSafe("b").join(","));   // b   (not "a,b")

// ✅ Expected output (last six lines):
// a,b
// true
// a
// a,b
// a
// b

🎯 Mini-Challenge: Build an Undo Stack

Undo/redo is the use case where deep copying stops being theory. Every step back in time is a snapshot you kept because you cloned instead of mutating. Build one. Nothing is filled in — you get the brief and a fixed test drive, and if your output matches the expected output your history is correct.

// 🎯 MINI-CHALLENGE: an undo stack
//
// Write createHistory(initialState) from scratch. Only the brief is here.
//
// It returns an object with four methods:
//   current()         -> the current state object
//   commit(changeFn)  -> push the current state onto an undo stack, deep-clone
//                        it, hand the CLONE to changeFn so it can be edited
//                        freely, and make that clone the new current state
//   undo()            -> pop the last state off the stack and make it current
//                        again; if the stack is empty, do nothing (no crash)
//   depth()           -> how many states are sitting on the undo stack
//
// The rule that matters: after any number of commits and undos, the object
// that was originally passed in must be completely unchanged.

// your code here


// --- Do not change the code below: this is the test drive ---
const state = { user: { name: "Sam", score: 100 }, tags: ["a"] };
const history = createHistory(state);

history.commit(s => { s.user.score = 250; });
history.commit(s => { s.tags.push("b"); });

console.log("now:", history.current().user.score, history.current().tags.join("/"));
console.log("depth:", history.depth());

history.undo();
console.log("after 1 undo:", history.current().user.score, history.current().tags.join("/"));

history.undo();
console.log("after 2 undos:", history.current().user.score, history.current().tags.join("/"));

history.undo();   // nothing left to undo — this must not crash
console.log("original untouched:", state.user.score, state.tags.join("/"));

// ✅ Expected output:
// now: 250 a/b
// depth: 2
// after 1 undo: 250 a
// after 2 undos: 100 a
// original untouched: 100 a

Mastery Summary

Mastering deep and shallow copying is essential for writing reliable JavaScript:

Good developers write code that avoids unintended side effects. Great developers predict them before they happen. Understanding deep vs shallow copying lets you design safer functions, cleaner components, and more stable architectures.

This is one of the most powerful skills for building production-level apps — and one every serious developer must understand.

Practice quiz

Which of these is stored by value (copied independently on assignment)?

  • An object
  • An array
  • A number
  • A Map

Answer: A number. Primitives like numbers are stored by value; objects, arrays, Maps, etc. are stored by reference.

let a = { score: 10 }; let b = a; b.score = 99; What is a.score?

  • 99
  • 10
  • undefined
  • Error

Answer: 99. b and a point to the same object reference, so changing b.score also changes a.score to 99.

How much of an object does a shallow copy duplicate?

  • Every nested level
  • Nothing
  • Only nested objects
  • Only the top level; nested objects stay shared

Answer: Only the top level; nested objects stay shared. A shallow copy duplicates only the top level and still shares references to nested values.

After const copy = { ...user }; copy.prefs.theme = 'light'; what is user.prefs.theme (started as 'dark')?

  • 'dark'
  • 'light'
  • undefined
  • Error

Answer: 'light'. Spread is a shallow copy, so the nested prefs object is shared and the change leaks to user.

Which built-in creates a true deep clone of nested data, Dates, Maps, and circular references?

  • structuredClone()
  • Object.assign()
  • Array.slice()
  • JSON.stringify()

Answer: structuredClone(). structuredClone() deep-clones nested objects/arrays and handles Dates, Map, Set, and circular references.

What does JSON.parse(JSON.stringify(obj)) do to a function property?

  • Keeps it
  • Converts it to a string
  • Removes it
  • Throws an error

Answer: Removes it. JSON cloning drops functions (and undefined) entirely.

What does JSON cloning turn Infinity into?

  • Infinity
  • null
  • 0
  • 'Infinity'

Answer: null. JSON.stringify converts Infinity (and NaN) to null.

What does JSON cloning do to a Date value?

  • Keeps it as a Date object
  • Removes it
  • Throws an error
  • Converts it to an ISO string

Answer: Converts it to an ISO string. A Date survives only as a string after JSON.parse(JSON.stringify(...)), losing its Date type.

In the recursive deepClone, what is the base case?

  • When the value is an array
  • When value is null or typeof value !== 'object' (a primitive)
  • When the object is empty
  • When depth reaches 10

Answer: When value is null or typeof value !== 'object' (a primitive). Primitives and null are returned as-is; only objects and arrays recurse.

What is the immutable update pattern?

  • Edit the original object in place
  • Freeze every object
  • Clone the data, change the clone, and return it
  • Avoid all copying

Answer: Clone the data, change the clone, and return it. Instead of mutating in place, you clone, modify the clone, and return it, keeping state predictable.

Continue this course