⚑IntermediateObject-Oriented JavaScript

Shallow vs Deep Copy

Why copying objects in JavaScript is not always what you expect, and how to do it correctly

Overview

In JavaScript, primitive values like numbers, strings, and booleans are copied by value β€” when you assign one to another variable, you get a completely independent copy. Objects and arrays work differently: they are stored by reference. When you assign an object to a new variable, both variables point to the same place in memory. Mutating the second variable also mutates the first, which is a constant source of bugs for developers who expect copying to behave the way it does with primitives. A shallow copy creates a new top-level object or array, but only copies the immediate properties. If any of those properties are themselves objects or arrays, the shallow copy holds a reference to the same nested objects as the original. Methods like the spread operator (`{...obj}`), `Object.assign()`, and `Array.prototype.slice()` all produce shallow copies. They are fast and work perfectly when your data is flat β€” but the moment you have nested structure, changes to a nested object affect both the copy and the original. A deep copy creates a completely independent clone of the entire structure, recursively copying every nested object and array. The classic quick approach is `JSON.parse(JSON.stringify(obj))`, but it silently drops functions, `undefined` values, `Symbol` keys, and corrupts `Date` objects into strings. The modern standard is `structuredClone()`, available in all current browsers and Node.js 17+, which handles `Date`, `Map`, `Set`, `RegExp`, and typed arrays correctly. Knowing when you need a shallow copy versus a deep copy β€” and which method to reach for β€” is one of the most practically useful skills in everyday JavaScript.

Key Points

  • β€’Primitives are copied by value; objects and arrays are copied by reference β€” assigning an object to a new variable does NOT create a copy
  • β€’Shallow copy: creates a new top-level container but nested objects/arrays still point to the same memory as the original
  • β€’`{...obj}`, `Object.assign({}, obj)`, and `[...arr]` / `arr.slice()` all produce shallow copies
  • β€’`structuredClone(obj)` is the modern standard for deep cloning β€” handles Date, Map, Set, RegExp, and circular structures
  • β€’`JSON.parse(JSON.stringify(obj))` is a quick deep clone but drops functions, undefined, Symbol keys, and converts Dates to strings
  • β€’Deep copy is required when you need to mutate nested state without affecting the original β€” common in React state updates

Examples

Reference problem β€” two variables pointing at the same object

const original = { name: 'Alice', scores: [95, 87, 91] };
const copy = original; // NOT a copy β€” same reference!

copy.name = 'Bob';
console.log(original.name); // 'Bob' β€” original was mutated too!

// To prove they are the same:
console.log(original === copy); // true

This is the root cause of most unexpected mutation bugs.

Shallow copy β€” top level is independent but nested array is shared

const original = { name: 'Alice', scores: [95, 87, 91] };
const shallow = { ...original }; // spread = shallow copy

shallow.name = 'Bob';         // OK β€” top-level string is independent
console.log(original.name);   // 'Alice' βœ“

shallow.scores.push(100);     // BAD β€” scores is still the same array!
console.log(original.scores); // [95, 87, 91, 100] β€” mutated!

Spread only goes one level deep.

Deep copy with structuredClone β€” fully independent

const original = {
  name: 'Alice',
  scores: [95, 87, 91],
  meta: { createdAt: new Date('2024-01-01') },
};

const deep = structuredClone(original);

deep.scores.push(100);
deep.meta.createdAt = new Date('2025-06-01');

console.log(original.scores);          // [95, 87, 91] βœ“ untouched
console.log(original.meta.createdAt);  // 2024-01-01  βœ“ untouched
console.log(deep.meta.createdAt instanceof Date); // true βœ“

JSON.parse/stringify β€” quick but lossy

const original = {
  name: 'Alice',
  greet: () => 'Hello',     // function
  createdAt: new Date(),    // Date
  score: undefined,         // undefined
};

const clone = JSON.parse(JSON.stringify(original));

console.log(clone.name);      // 'Alice'             βœ“
console.log(clone.greet);     // undefined           βœ— function dropped
console.log(typeof clone.createdAt); // 'string'     βœ— Date β†’ ISO string
console.log('score' in clone); // false              βœ— undefined dropped

Use structuredClone instead of JSON tricks whenever possible.

Practical: immutable state update pattern (React-style)

// Simulating React state: never mutate the existing state object
const state = {
  user: { name: 'Alice', age: 30 },
  settings: { theme: 'dark', lang: 'en' },
};

// WRONG β€” mutates state directly
state.user.age = 31;

// RIGHT β€” shallow copy at every level you're changing
const nextState = {
  ...state,            // copy top level
  user: {
    ...state.user,     // copy the nested object being changed
    age: 31,           // then override just what changed
  },
};

console.log(state.user.age);     // 30 βœ“ original unchanged
console.log(nextState.user.age); // 31 βœ“
console.log(nextState.settings === state.settings); // true (shared β€” unchanged)

For deeply nested trees, structuredClone can be simpler than chaining spreads.

Common Mistakes

  • Assuming `{...obj}` creates a deep copy β€” it only copies one level; nested objects are still shared by reference
  • Using `JSON.parse(JSON.stringify(obj))` on data that contains Date objects, functions, undefined, or Map/Set β€” these values are silently lost or corrupted
  • Mutating an object passed as a function argument β€” the caller's object is modified too because JavaScript passes objects by reference
  • Forgetting to copy nested arrays when doing manual spread updates β€” `{...state, items: state.items}` still shares the items array

Note

Default to `structuredClone()` for deep clones β€” it is fast, handles all built-in types correctly, and requires no libraries. Reserve `JSON.parse(JSON.stringify())` only for environments where `structuredClone` is unavailable and your data is plain JSON-safe.

Exercises

Write JavaScript code to solve each exercise. Run it and check your output.

Loading editor…

Completed: 0 / 6 exercises

Knowledge Check

Question 1 of 4

What does the spread operator `{...obj}` produce?