Introduction

Copying objects and arrays in JavaScript can be tricky. Understanding the difference between shallow copy and deep clone is essential to avoid bugs and unexpected behavior.

The Problem

Assignment Doesn’t Copy

const original = { name: "Alice", age: 30 };
const copy = original;

copy.age = 31;

console.log(original.age); // 31 - Changed!
console.log(copy.age); // 31

// Both reference the same object!

Assignment creates a reference, not a copy.

Shallow Copy

Shallow copy creates a new object, but nested objects/arrays are still referenced.

Methods for Shallow Copy

1. Spread Operator

const original = {
  name: "Alice",
  address: { city: "Stockholm" },
};

const shallow = { ...original };

shallow.name = "Bob";
console.log(original.name); // 'Alice' - unchanged ✓

shallow.address.city = "Oslo";
console.log(original.address.city); // 'Oslo' - changed! ✗

2. Object.assign()

const original = { name: "Alice", age: 30 };
const shallow = Object.assign({}, original);

3. Array.slice() / Array.from()

const original = [1, 2, { value: 3 }];
const shallow = original.slice();
// or
const shallow = Array.from(original);

When Shallow Copy Works

Flat objects/arrays:

const flat = { a: 1, b: 2, c: 3 };
const copy = { ...flat }; // Works perfectly

Primitive values:

const data = { name: "Alice", age: 30 };
const copy = { ...data }; // All primitives copied

Deep Clone

Deep clone creates a completely independent copy, including all nested objects/arrays.

Methods for Deep Clone

1. JSON.parse(JSON.stringify())

const original = {
  name: "Alice",
  address: { city: "Stockholm" },
  hobbies: ["reading", "coding"],
};

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

deep.address.city = "Oslo";
console.log(original.address.city); // 'Stockholm' - unchanged ✓

Limitations:

  • ❌ Loses functions
  • ❌ Loses undefined
  • ❌ Loses Symbols
  • ❌ Loses Date objects (becomes strings)
  • ❌ Circular references cause errors

2. Structured Clone (Modern)

const original = {
  name: "Alice",
  date: new Date(),
  nested: { value: 42 },
};

const deep = structuredClone(original);

Supported in:

  • Node.js 17+
  • Modern browsers
  • Handles Dates, Maps, Sets, etc.

3. Lodash cloneDeep()

import _ from "lodash";

const deep = _.cloneDeep(original);

4. Custom Implementation

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof Array) return obj.map((item) => deepClone(item));
  if (typeof obj === "object") {
    const cloned = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        cloned[key] = deepClone(obj[key]);
      }
    }
    return cloned;
  }
}

Comparison

Example

const original = {
  name: "Alice",
  address: {
    city: "Stockholm",
    country: "Sweden",
  },
  hobbies: ["reading", "coding"],
};

// Shallow copy
const shallow = { ...original };

// Deep clone
const deep = JSON.parse(JSON.stringify(original));

Modifying Nested Properties

// Modify nested property
shallow.address.city = "Oslo";
deep.address.city = "Oslo";

console.log(original.address.city); // 'Oslo' (shallow) vs 'Stockholm' (deep)

When to Use What?

Use Shallow Copy When:

Flat structures

const config = { theme: "dark", language: "en" };
const copy = { ...config };

Performance matters

// Shallow copy is faster
const items = [...array];

Nested objects don’t need copying

const user = { name: "Alice", metadata: sharedMetadata };
const copy = { ...user }; // metadata reference is OK

Use Deep Clone When:

Nested objects need independence

const order = {
  items: [{ product: "A", qty: 1 }],
  customer: { name: "Alice" },
};
const copy = structuredClone(order);

Modifying nested structures

const template = { settings: { theme: "dark" } };
const user1 = structuredClone(template);
const user2 = structuredClone(template);
// Each can modify settings independently

Complex objects

const state = {
  users: [{ id: 1, profile: { bio: "..." } }],
  config: { api: { endpoint: "..." } },
};
const backup = structuredClone(state);

Performance Comparison

MethodSpeedLimitations
Spread (...)FastShallow only
Object.assign()FastShallow only
JSON.parse/stringifyMediumMany limitations
structuredClone()MediumBest modern option
Lodash cloneDeepSlowMost reliable

Common Pitfalls

1. Nested Arrays

const matrix = [
  [1, 2],
  [3, 4],
];
const shallow = [...matrix];

shallow[0][0] = 99;
console.log(matrix[0][0]); // 99 - changed!

Solution: Deep clone for nested arrays.

2. Circular References

const obj = { name: "Alice" };
obj.self = obj; // Circular!

// JSON.parse/stringify fails
JSON.parse(JSON.stringify(obj)); // Error!

// structuredClone handles it
structuredClone(obj); // Works!

3. Functions

const obj = {
  name: "Alice",
  greet: () => console.log("Hello"),
};

const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned.greet); // undefined - lost!

Best Practices

✅ Do This

1. Use shallow copy for flat objects

const flat = { a: 1, b: 2 };
const copy = { ...flat };

2. Use structuredClone for nested objects

const nested = { a: { b: { c: 1 } } };
const copy = structuredClone(nested);

3. Consider immutable libraries

import { produce } from "immer";

const nextState = produce(currentState, (draft) => {
  draft.items.push(newItem);
});

❌ Don’t Do This

1. Don’t use shallow copy for nested objects

// ❌ Wrong for nested
const nested = { a: { b: 1 } };
const copy = { ...nested };
copy.a.b = 2; // Modifies original!

2. Don’t use JSON for objects with functions

// ❌ Loses functions
const obj = { fn: () => {} };
JSON.parse(JSON.stringify(obj)); // fn is gone!

Tools

Use our tools:

Conclusion

Understanding copy vs clone:

Shallow copy:

  • Faster
  • Copies top level only
  • Good for flat structures

Deep clone:

  • Slower
  • Copies everything recursively
  • Necessary for nested structures

Choose based on:

  • Data structure complexity
  • Performance requirements
  • Need for independence

Next Steps