In JavaScript, objects are reference types. When you write const b = a, both variables point to the same object mutating b mutates a. A shallow copy (Object.assign, spread operator) copies one level deep, but nested objects are still shared. For true independence between copies, you need a deep clone where every nested object and array is duplicated. JSON provides two common approaches: the classic JSON.parse(JSON.stringify()) trick and the modern structuredClone().
Method 1: JSON.parse(JSON.stringify())
// The JSON deep copy pattern
const original = {
user: { name: 'Alice', age: 30 },
tags: ['developer', 'typescript'],
settings: { theme: 'dark', notifications: true }
};
const deep = JSON.parse(JSON.stringify(original));
// Prove independence:
deep.user.name = 'Bob';
deep.tags.push('golang');
console.log(original.user.name); // 'Alice' unaffected “
console.log(original.tags); // ['developer', 'typescript'] “
// How it works:
// 1. JSON.stringify(original) …'{"user":{"name":"Alice",...}}'
// 2. JSON.parse('{"user":...}') …brand new object with all new references
What JSON.parse/stringify CANNOT Clone
const tricky = {
fn: () => 'hello', // Function
date: new Date('2026-05-01'), // Date object
undef: undefined, // undefined value
map: new Map([['key', 'value']]), // Map
set: new Set([1, 2, 3]), // Set
regex: /hello/gi, // RegExp
bignum: 9007199254740993n, // BigInt
symbol: Symbol('id'), // Symbol
circular: null // Will set below
};
// Circular reference throws error
const a = {};
a.self = a;
JSON.stringify(a); // TypeError: Converting circular structure to JSON
// What JSON.parse/stringify DOES to tricky:
const copy = JSON.parse(JSON.stringify(tricky));
console.log(copy.fn); // undefined functions are dropped
console.log(copy.date); // '2026-05-01T00:00:00.000Z' (string, not Date!)
console.log(copy.undef); // key dropped entirely
console.log(copy.map); // {} Map becomes empty object
console.log(copy.set); // {} Set becomes empty object
console.log(copy.regex); // {} RegExp becomes empty object
Method 2: structuredClone() The Modern Way
// Supported in Node.js 17+, all modern browsers
const tricky = {
date: new Date('2026-05-01'),
map: new Map([['key', 'value'], ['count', 42]]),
set: new Set([1, 2, 3, 4, 5]),
regex: /hello/gi,
buffer: new ArrayBuffer(8),
nested: { array: [1, [2, [3]]] }
};
const deep = structuredClone(tricky);
// structuredClone preserves:
console.log(deep.date instanceof Date); // true “
console.log(deep.map instanceof Map); // true “(full contents preserved)
console.log(deep.set instanceof Set); // true “
console.log([...deep.set]); // [1, 2, 3, 4, 5] “
// Circular references are handled:
const circular = { name: 'loop' };
circular.self = circular;
const copy = structuredClone(circular);
console.log(copy.self === copy); // true “ circular ref preserved
// structuredClone CANNOT handle:
// - Functions (throws DataCloneError)
// - Error objects lose their stack trace
// - DOM nodes and class instances lose prototype methods
Method 3: Library Approaches
// Lodash cloneDeep most permissive, handles everything
import { cloneDeep } from 'lodash';
class User {
constructor(name) {
this.name = name;
}
greet() { return `Hello, ${this.name}`; }
}
const obj = {
user: new User('Alice'),
fn: () => 'hello',
nested: { deep: { value: 42 } }
};
const copy = cloneDeep(obj);
console.log(copy.user instanceof User); // true “
console.log(copy.user.greet()); // 'Hello, Alice' “ prototype preserved
console.log(copy.fn()); // 'hello' “ functions cloned
// For class instances without lodash: manual copy constructor
class Config {
constructor(data) {
Object.assign(this, JSON.parse(JSON.stringify(data)));
}
clone() {
return new Config(this);
}
}
Comparison: When to Use Each
| Method | Functions | Date | Map/Set | Circular Refs | Speed | Browser Support |
|---|---|---|---|---|---|---|
| JSON.parse/stringify | Dropped | …String | …{} | Throws | Fast | All browsers |
| structuredClone() | Throws | Preserved | Preserved | Handled | Fastest | Node 17+, modern browsers |
| lodash cloneDeep | Cloned (ref) | Preserved | Preserved | Handled | Slower | Any (npm) |
| Manual recursion | Custom | Custom | Custom | Custom | Varies | Any |
Practical Rules
Use JSON.parse(JSON.stringify()) for plain data objects with no special types API responses, database query results, configuration objects. Use structuredClone() as the modern default for objects containing Dates, Maps, Sets, or circular references. Use lodash.cloneDeep when you need to preserve class instances and prototype chains. Write a manual clone function when you control the data structure and need maximum performance.