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.
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
| Method | Complexity | Best For |
|---|---|---|
| Spread operator | O(n) | Flat objects, small data |
| structuredClone | O(n) | Large nested objects |
| JSON method | O(n) | Simple JSON-safe data |
| Lodash cloneDeep | O(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:
| Type | structuredClone | JSON 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:
- Objects are reference types - assignment alone doesn't create a copy
- Use spread operator for simple flat objects (shallow copy)
- Use structuredClone() for deep copying in modern JavaScript
- Consider TypeScript for type-safe cloning utilities
- 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.