How to copy an Object in JavaScript?
Copying an object in JavaScript creates a new object with the same data, but the behavior depends on whether you do a shallow or deep copy.
Theory
TL;DR
- Shallow copy is like photocopying a letter with a sealed envelope: you get the letter, but the envelope still points to the original contents
- Deep copy photocopies everything, envelope included, with new contents inside
- Shallow only duplicates the top level; nested objects stay as shared references
- Flat object with no nesting: use
{...obj}. Nested objects that change independently: usestructuredClone JSON.parse(JSON.stringify(obj))is deep but drops functions,Date,Map, andundefined
Quick example
const original = { user: { name: 'Alice' }, id: 1 };
// Shallow - nested object is a shared reference
const shallow = { ...original };
shallow.user.name = 'Bob';
console.log(original.user.name); // "Bob" - original was mutated!
// Deep - nested object is fully independent
const deep = structuredClone(original);
deep.user.name = 'Charlie';
console.log(original.user.name); // "Alice" - original is safeChanging shallow.user.name also changes original.user.name because both point to the same object in memory. structuredClone breaks that link entirely.
The core difference
Shallow copy duplicates only the top-level property values. Primitives (strings, numbers, booleans) get copied by value, so they are independent. But nested objects and arrays are copied by reference: both the original and the copy point to the same memory address. Change the nested data through the copy, and the original changes too. Deep copy walks the entire structure recursively and creates new objects at every level. No shared references. Mutations stay isolated.
When to use
- Flat config object with no nesting:
{...obj}orObject.assign({}, obj) - React state with nested objects:
structuredClone(state)before updating - JSON-only data (no Dates, functions, Maps):
JSON.parse(JSON.stringify(obj)) - Object contains
Date,Map, orSet:structuredClone(Chrome 98+, Node 17.5+) - Legacy environment or complex custom classes:
lodash.cloneDeep - Shared read-only data that never mutates: shallow is fine and faster
Comparison table
| Method | Type | Handles Functions? | Handles Date/Map/Set? | Performance |
|---|---|---|---|---|
{...obj} | Shallow | No | No (copies reference) | Fastest |
Object.assign({}, obj) | Shallow | No | No (copies reference) | Fast |
JSON.parse(JSON.stringify(obj)) | Deep | No (drops them) | No (Date becomes string) | Medium |
structuredClone(obj) | Deep | No (throws error) | Yes | Fast |
lodash.cloneDeep(obj) | Deep | Yes | Yes | Slowest |
How V8 handles copying
Spread and Object.assign iterate own enumerable keys using the [[GetOwnPropertyKeys]] internal method and copy values by reference without recursion. That is why they are fast, and that is why they stop at the first level. JSON.stringify serializes the object to a string, traversing the structure and skipping non-enumerable properties, functions, and undefined. Then JSON.parse rebuilds a new object tree from scratch. structuredClone uses the HTML Structured Clone algorithm, the same mechanism behind postMessage, which handles Date, Map, Set, and Blob but explicitly rejects functions and WeakMap.
Common mistakes
1. Assuming spread copies nested objects
// Buggy
const state = { user: { prefs: {} } };
const copy = { ...state };
copy.user.prefs.dark = true;
console.log(state.user.prefs.dark); // true - state was mutated
// Fix: structuredClone(state)2. Using JSON on functions or undefined
JSON.stringify({ fn: () => {}, undef: undefined });
// Result: "{}" - both properties disappear without any error
// Fix: structuredClone or lodash.cloneDeep3. Date becomes a string after JSON roundtrip
const obj = { created: new Date() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.created instanceof Date); // false - it is a string now
// Fix: structuredClone keeps the Date instance intact4. Passing the target object directly to Object.assign
// Buggy - modifies myObject directly
Object.assign(myObject, source);
// Fix
Object.assign({}, myObject, source);5. Circular references
const obj = {};
obj.self = obj;
JSON.stringify(obj); // RangeError: circular structure
structuredClone(obj); // DataCloneError
// Fix: lodash.cloneDeep handles circular refs correctlyReal-world usage
- React:
structuredClone(state)for immutable nested state updates without mutation - Redux Toolkit: uses Immer internally, but
structuredCloneworks for serializable state migration - Express middleware:
const data = structuredClone(req.body)to avoid mutating the original request - Node.js: spread for simple config merging,
structuredClonefor worker thread data transfer - Lodash:
cloneDeepused in over 1000 npm packages as the safe default for legacy projects
In my experience, the most common production bug from this topic is mutating React state through a shallow copy of a nested object. The component re-renders but shows stale data because the reference to the nested object never actually changed.
Follow-up questions
Q: What does original.b.c print if you do const copy = { ...{ a: 1, b: { c: 2 } } } and then copy.b.c = 3?
A: original.b.c becomes 3. Spread copies only the top level, so b is still a shared reference.
Q: Why does JSON.stringify drop functions?
A: JSON is a text format defined by RFC 8259. Functions are not part of that spec, so JSON.stringify skips them entirely without throwing.
Q: Implement a shallow copy without any built-ins.
A: Object.keys(obj).reduce((acc, k) => { acc[k] = obj[k]; return acc; }, {}) iterates own keys and copies values one by one.
Q: When does structuredClone throw?
A: It throws DataCloneError for non-clonable types: WeakMap, WeakSet, functions, and DOM nodes. It also throws on circular references, unlike lodash.cloneDeep which handles them.
Q: What happens to prototype properties and non-enumerable props with spread?
A: Both are lost. Spread copies only own enumerable keys. If the original was created with Object.create(proto), the copy has no prototype link. Properties defined with Object.defineProperty(..., { enumerable: false }) are skipped too.
Examples
Shallow copy trap in a config object
const config = {
server: { host: 'localhost', port: 3000 },
timeout: 5000
};
const devConfig = { ...config };
devConfig.timeout = 8000; // OK - primitive, independent copy
devConfig.server.port = 4000; // Bad - mutates the original!
console.log(config.timeout); // 5000 - safe
console.log(config.server.port); // 4000 - mutatedChanging timeout is safe because it is a primitive and primitives are copied by value. Changing server.port mutates the original because config.server and devConfig.server point to the same object. To protect nested data, replace spread with structuredClone(config).
React state update with structuredClone
const [userData, setUserData] = useState({
profile: { name: 'Alice', settings: { theme: 'dark' } }
});
// Wrong: spread does not protect nested settings
const badUpdate = { ...userData };
badUpdate.profile.settings.theme = 'light';
setUserData(badUpdate); // original profile.settings was already mutated here
// Correct: structuredClone creates a fully independent copy
const goodUpdate = structuredClone(userData);
goodUpdate.profile.settings.theme = 'light';
setUserData(goodUpdate);
// userData.profile.settings.theme stays 'dark' until React processes the updateThe wrong version mutates state before setUserData is even called. React may not detect the change because the reference to profile did not change, so the old and new state objects look identical to the reconciler.
Edge case: prototypes, non-enumerable properties, and circular references
// Prototype and non-enumerable
const original = Object.create({ proto: 'shared' });
original.own = { nest: 'value' };
Object.defineProperty(original, 'hidden', { value: 'secret', enumerable: false });
const spread = { ...original };
console.log(spread.proto); // undefined - prototype link lost
console.log(spread.hidden); // undefined - non-enumerable skipped
const json = JSON.parse(JSON.stringify(original));
console.log(json.proto); // undefined
const cloned = structuredClone(original);
console.log(cloned.proto); // 'shared' - structuredClone preserves the prototype chain
// Circular reference
const circular = { name: 'test' };
circular.self = circular;
// JSON.stringify(circular) - RangeError
// structuredClone(circular) - DataCloneError
import _ from 'lodash';
const safe = _.cloneDeep(circular); // handles it correctlyOnly structuredClone preserves the prototype chain from Object.create. Only lodash.cloneDeep survives circular references without throwing. These two edge cases are where spread and JSON both fail silently or with errors.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.