DevPath · Learn to code ESPTEN

Functional programming

Shallow vs deep copy

The illusion of the shallow copy

When you copy an object with the spread ({ ...obj }) or with Object.assign({}, obj), JavaScript copies only the first level. If some property is itself an object or an array, what gets copied is the reference, not its contents. The copy and the original share that nested object.

Think of a photocopy of a photo album: the photocopy has its own cover, but instead of copying the photos it sticks on a post-it that says "the photos are in the original album". If someone scratches out a photo, it shows up scratched in both.

const original = {
  name: "Ann",
  address: { city: "Madrid" },
};

const copy = { ...original };
copy.name = "Bea";           // affects ONLY the copy (first level)
copy.address.city = "Bilbao"; // it ALSO affects the original!

console.log(original.name);         // "Ann"    (good)
console.log(original.address.city); // "Bilbao" (bug!)

name is a string (primitive) and was truly copied. But address is an object: both share it. Changing one changes the other.

Deep copy

A deep copy recursively clones every level, so that the copy is completely independent of the original. There are three common paths:

1. structuredClone (the modern way)

It is a native function of the browser and of modern Node that clones deeply:

const copy = structuredClone(original);
copy.address.city = "Bilbao";
console.log(original.address.city); // "Madrid" (untouched)

It supports objects, arrays, Map, Set, dates... but not functions.

2. JSON (for simple data)

Converting to text and back creates a deep copy in one line:

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

It is handy, but it only works for "JSON-pure" data: it loses functions, undefined, dates (they become strings), Map/Set, etc.

3. Recursion by hand

Walk the structure and clone each level. It is what the two options above do internally:

function clone(value) {
  if (Array.isArray(value)) return value.map(clone);
  if (value && typeof value === "object") {
    const output = {};
    for (const key of Object.keys(value)) output[key] = clone(value[key]);
    return output;
  }
  return value; // primitives: returned as-is
}

Freezing to prevent mutations: Object.freeze

If you want to guarantee that an object is not modified, Object.freeze makes it read-only: any attempt to change it is ignored (or throws an error in strict mode).

const config = Object.freeze({ theme: "dark" });
config.theme = "light";     // ignored
console.log(config.theme);  // "dark"

Careful: Object.freeze is also shallow. It freezes the first level, but nested objects stay mutable unless you freeze them yourself too (a recursive "deep freeze").

Examples

The shallow-copy bug

const original = { level: 1, data: { points: [10, 20] } };
const copy = { ...original };
copy.data.points.push(30); // mutates the SHARED array
console.log("original:", original.data.points); // [10, 20, 30] contaminated!
console.log("copy:    ", copy.data.points);     // [10, 20, 30]

Deep copy with structuredClone

const original = { level: 1, data: { points: [10, 20] } };
const copy = structuredClone(original);
copy.data.points.push(30);
console.log("original:", original.data.points); // [10, 20] untouched
console.log("copy:    ", copy.data.points);     // [10, 20, 30]

Object.freeze prevents mutation

const settings = Object.freeze({ volume: 5 });
settings.volume = 11; // silently ignored
console.log(settings.volume); // 5
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Composition and curryingView the module →