Copy Objects in JavaScript: A Complete Guide

Master shallow copy, deep copy, and structuredClone with practical examples for modern web development

Copying objects in JavaScript is one of those fundamentals that seems simple at first but reveals hidden complexity once you start working with nested structures. Whether you're managing state in a React application, preserving immutable data patterns, or simply need to create an independent copy of an object, understanding how object copying works--and when it doesn't work as expected--is essential for writing reliable JavaScript code. This guide covers every method available, from the basics to the modern structuredClone API, with TypeScript considerations throughout.

What You'll Learn

  • How object references work in JavaScript
  • When to use shallow copy vs deep copy
  • Modern solutions like structuredClone()
  • TypeScript type-safe cloning patterns
  • Performance optimization strategies
  • Common pitfalls and edge cases

Object Copying in JavaScript

3

Main Copying Methods

1

Native Deep Clone API

100%

Reference Type Behavior

Understanding Object Reference vs Value

In JavaScript, objects are reference types, not primitive values. This fundamental concept is crucial to understanding object copying--and it ties directly into how Application Programming Interfaces (APIs) handle data exchange and object serialization.

When you assign an object to a new variable, you're copying the memory address (reference), not the actual object data. Both variables point to the same object in memory--modifying one affects the other.

// Assignment copies the reference, not the value
const original = { name: 'John', age: 30 };
const copy = original;

copy.age = 31;

console.log(original.age); // 31 - original was also modified!
console.log(copy.age); // 31

In contrast, primitives (strings, numbers, booleans, null, undefined, Symbol, BigInt) are copied by value--each assignment creates a completely independent copy.

// Primitives are copied by value
let a = 10;
let b = a;
b = 20;

console.log(a); // 10 - unchanged
console.log(b); // 20

This reference behavior is what makes object copying essential to understand--without it, you'll encounter unexpected mutations that cause bugs in your applications.

Shallow Copy: Surface-Level Duplication

A shallow copy creates a new object and copies the top-level properties from the original. However, nested objects and arrays are still copied by reference--both the original and the copy share the same nested objects.

Spread Operator (Modern ES6+)

The spread operator provides a clean syntax for shallow copying:

const original = { 
 name: 'John',
 address: { city: 'Toronto', country: 'Canada' }
};

const shallowCopy = { ...original };

// Top-level properties are independent
shallowCopy.name = 'Jane';
console.log(original.name); // 'John' - unchanged

// Nested objects are still shared!
shallowCopy.address.city = 'Vancouver';
console.log(original.address.city); // 'Vancouver' - also changed!

Object.assign() (ES6)

The Object.assign() method works similarly but has a slightly different syntax:

const original = { name: 'John' };
const copy = Object.assign({}, original);

// Equivalent to: const copy = { ...original };

When Shallow Copy Is Sufficient

Shallow copy works well when:

  • Your object has no nested structures (all values are primitives)
  • You specifically want to share nested object references
  • Performance is critical and objects are large

For most real-world applications with nested data, you'll need a deep copy instead.

Following SOLID principles in JavaScript often involves creating immutable data structures, which requires understanding when shallow copy is appropriate versus when deep copy is necessary.

Deep Copy: Complete Independence

A deep copy recursively copies all levels of an object, creating a completely independent copy with no shared references.

structuredClone() (Modern Native API)

The structuredClone() function is the modern native solution available in all modern browsers and Node.js (v17+):

const original = {
 name: 'John',
 address: { city: 'Toronto', country: 'Canada' },
 hobbies: ['coding', 'hiking']
};

// Deep clone using structuredClone
const deepCopy = structuredClone(original);

// Now everything is independent
deepCopy.address.city = 'Vancouver';
deepCopy.hobbies.push('reading');

console.log(original.address.city); // 'Toronto' - unchanged
console.log(original.hobbies); // ['coding', 'hiking'] - unchanged

Advantages of structuredClone():

  • Handles more data types than JSON method
  • Better performance for large objects
  • Supports transferable objects for zero-copy semantics
  • Native browser/Node.js support (no dependencies)

JSON Serialization Method (Legacy)

The classic approach using JSON.parse(JSON.stringify()) still works but has limitations:

const original = { name: 'John', date: new Date() };

// Works but loses Date objects, functions, undefined, Symbols
const copy = JSON.parse(JSON.stringify(original));

console.log(copy.date); // String representation, not Date object!

What JSON method loses:

  • Functions
  • undefined values
  • Symbol-keyed properties
  • Date objects (converted to strings)
  • RegExp, Map, Set (become empty objects)
  • Circular references (throws error)

Lodash cloneDeep (External Library)

For projects that need maximum compatibility with edge cases:

import { cloneDeep } from 'lodash';

const original = { /* complex object with special types */ };
const copy = cloneDeep(original);

// Handles: functions, Symbols, circular references, Date, RegExp, Map, Set, etc.

Considerations: Adds bundle size (~70KB minified), but lodash-es enables tree-shaking.

When building microservices with Node.js, proper object cloning becomes essential for maintaining data isolation between services.

Copying Methods Comparison

Choose the right method for your use case

Spread Operator

Modern ES6+ syntax. Best for flat objects. Shallow copy only.

Object.assign()

ES6 method. Similar to spread. Supports property merging.

structuredClone()

Native API. Deep copy for most use cases. Best performance.

JSON Method

Legacy fallback. Simple but limited. Loses functions and dates.

Lodash cloneDeep

External library. Maximum compatibility. Handles all edge cases.

Custom Solution

Tailor-made for specific requirements. Full control over behavior.

TypeScript: Type-Safe Object Cloning

TypeScript adds another dimension to object copying--type safety. Here's how to clone objects while preserving their types.

Generic Clone Functions

Create reusable, type-safe clone utilities:

function cloneObject<T extends object>(obj: T): T {
 return structuredClone(obj);
}

interface User {
 id: number;
 name: string;
 email: string;
}

const user: User = { id: 1, name: 'John', email: '[email protected]' };
const userCopy = cloneObject(user);

// TypeScript knows userCopy is User
userCopy.name = 'Jane'; // ✓ Type-safe

Preserving Readonly Types

For immutable state patterns, clone while preserving readonly:

function cloneReadonly<T extends object>(obj: T): Readonly<T> {
 return Object.freeze(structuredClone(obj));
}

const original = { name: 'John', nested: { value: 42 } };
const frozen = cloneReadonly(original);

// frozen.nested.value = 100; // Error: Cannot assign to 'value' because it is a read-only property

Intersection Types and Merging

When combining objects, preserve type information:

interface BaseConfig {
 apiUrl: string;
 timeout: number;
}

interface DebugConfig {
 debug: boolean;
 logLevel: 'info' | 'warn' | 'error';
}

function mergeConfigs<T extends object, U extends object>(a: T, b: U): T & U {
 return { ...a, ...b };
}

const base: BaseConfig = { apiUrl: 'https://api.example.com', timeout: 5000 };
const debug: DebugConfig = { debug: true, logLevel: 'info' };

const merged = mergeConfigs(base, debug);
// Type: BaseConfig & DebugConfig

Implementing these patterns aligns with SOLID principles in JavaScript, particularly the Single Responsibility Principle--each clone function has one clear purpose.

Performance Considerations

Different copying methods have different performance characteristics. Here's how to choose wisely.

Performance Comparison

MethodComplexityBest For
Spread operatorO(n)Flat objects, small data
structuredCloneO(n)Large nested objects
JSON methodO(n)Simple JSON-safe data
Lodash cloneDeepO(n)Complex edge cases

Performance Optimization Tips

1. Use structuredClone for Large Objects

// structuredClone is faster than JSON for most cases
const largeObject = { /* thousands of nested properties */ };
const copy = structuredClone(largeObject);

2. Transferables for Zero-Copy Semantics

When working with ArrayBuffers or transferable objects:

const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const copy = structuredClone(buffer, [buffer]);
// buffer is now transferred (empty) - no copying!

3. Shallow Copy When Possible

If you only need top-level isolation:

// Much faster than deep copy for large objects with small nested parts
const shallow = { ...largeObject }; // Only copies references

Choosing the Right Method

  • Simple flat objects: Spread operator const copy = { ...obj }
  • Nested structures without special types: structuredClone()
  • Legacy browser support: JSON.parse(JSON.stringify()) or lodash
  • Maximum edge case coverage: lodash cloneDeep()
  • Maximum performance with large data: structuredClone() with transferables

Understanding these performance implications is crucial when building microservices with Node.js, where efficient data handling between services directly impacts system performance.

Common Pitfalls and Edge Cases

Object copying has several edge cases that can cause unexpected behavior.

Circular References

Objects that reference themselves (directly or indirectly) break naive cloning:

const obj = { name: 'circular' };
obj.self = obj; // Circular reference

// structuredClone handles this automatically
const copy = structuredClone(obj);

console.log(copy.self === obj); // false - properly cloned!
console.log(copy.self === copy); // true - self-reference preserved

Special Object Types

Not all objects are plain objects. These require special handling:

TypestructuredCloneJSON Method
Date✓ Preserved✗ String
RegExp✓ Preserved✗ Empty object
Map✓ Preserved✗ Empty object
Set✓ Preserved✗ Empty object
TypedArray✓ Preserved✗ Array of numbers

Symbol-Keyed Properties

const sym = Symbol('secret');
const obj = { [sym]: 'hidden value', regular: 'visible' };

// structuredClone preserves Symbols
const copy = structuredClone(obj);
console.log(copy[sym]); // 'hidden value'

// JSON method loses Symbols
const jsonCopy = JSON.parse(JSON.stringify(obj));
console.log(jsonCopy[sym]); // undefined

Prototype Chain

Plain object copies don't include the prototype:

function User(name) {
 this.name = name;
}
User.prototype.role = 'user';

const original = new User('John');
const copy = { ...original };

console.log(original.role); // 'user' (from prototype)
console.log(copy.role); // undefined (prototype lost!)

// Preserve prototype with Object.getOwnPropertyDescriptors
const copyWithProto = Object.create(
 Object.getPrototypeOf(original),
 Object.getOwnPropertyDescriptors(original)
);
console.log(copyWithProto.role); // 'user'

Frequently Asked Questions

Summary

Object copying in JavaScript requires understanding the difference between reference types and values, and knowing when to use shallow vs deep copying.

Key Takeaways:

  1. Objects are reference types - assignment alone doesn't create a copy
  2. Use spread operator for simple flat objects (shallow copy)
  3. Use structuredClone() for deep copying in modern JavaScript
  4. Consider TypeScript for type-safe cloning utilities
  5. Watch for edge cases like circular references and special object types

For most modern web development, structuredClone() provides the best balance of performance, simplicity, and compatibility. Reserve lodash cloneDeep for projects with complex edge case requirements or legacy browser support needs.


Related Resources

Need Custom Web Development?

Our team builds high-performance web applications using modern JavaScript frameworks and best practices.