Skip to main content

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: use structuredClone
  • JSON.parse(JSON.stringify(obj)) is deep but drops functions, Date, Map, and undefined

Quick example

javascript
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 safe

Changing 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} or Object.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, or Set: 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

MethodTypeHandles Functions?Handles Date/Map/Set?Performance
{...obj}ShallowNoNo (copies reference)Fastest
Object.assign({}, obj)ShallowNoNo (copies reference)Fast
JSON.parse(JSON.stringify(obj))DeepNo (drops them)No (Date becomes string)Medium
structuredClone(obj)DeepNo (throws error)YesFast
lodash.cloneDeep(obj)DeepYesYesSlowest

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

javascript
// 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

javascript
JSON.stringify({ fn: () => {}, undef: undefined }); // Result: "{}" - both properties disappear without any error // Fix: structuredClone or lodash.cloneDeep

3. Date becomes a string after JSON roundtrip

javascript
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 intact

4. Passing the target object directly to Object.assign

javascript
// Buggy - modifies myObject directly Object.assign(myObject, source); // Fix Object.assign({}, myObject, source);

5. Circular references

javascript
const obj = {}; obj.self = obj; JSON.stringify(obj); // RangeError: circular structure structuredClone(obj); // DataCloneError // Fix: lodash.cloneDeep handles circular refs correctly

Real-world usage

  • React: structuredClone(state) for immutable nested state updates without mutation
  • Redux Toolkit: uses Immer internally, but structuredClone works for serializable state migration
  • Express middleware: const data = structuredClone(req.body) to avoid mutating the original request
  • Node.js: spread for simple config merging, structuredClone for worker thread data transfer
  • Lodash: cloneDeep used 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

javascript
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 - mutated

Changing 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

javascript
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 update

The 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

javascript
// 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 correctly

Only 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?