When working with objects and arrays in JavaScript, it's common to encounter scenarios where you need to duplicate data. However, how JavaScript handles data types—specifically the distinction between primitive values and object references—can lead to unexpected behavior if not understood properly. This is where the concepts of shallow copy and deep copy become crucial.
Understanding the difference between these two copying mechanisms is fundamental to preventing unintended side effects and ensuring the integrity of your data, especially in applications dealing with complex state management or immutable data patterns.
Understanding How JavaScript Handles References
In JavaScript, primitive data types (like strings, numbers, booleans, null, undefined, and Symbol) are assigned and copied by value. This means when you assign a primitive to a new variable, a completely independent copy of that value is created.
However, objects and arrays are non-primitive data types, and they are assigned and copied by reference. When you assign an object to a new variable, both variables point to the same underlying object in memory. Modifying the object through either variable will affect the original object.
let originalNumber = 10;
let copiedNumber = originalNumber; // Copied by value
copiedNumber = 20;
console.log(originalNumber); // 10 (original unaffected)
let originalObject = { name: "Alice", age: 30 };
let copiedObject = originalObject; // Copied by reference
copiedObject.age = 31;
console.log(originalObject.age); // 31 (original affected)
console.log(originalObject === copiedObject); // true (they are the same object)
This reference-based behavior is the core reason why we need explicit copying mechanisms: to create new, independent objects.
Shallow Copy: A Surface-Level Replication
A shallow copy creates a new object or array, but it only copies the top-level properties. If any of the properties are themselves objects or arrays (i.e., nested structures), their references are copied, not the objects/arrays themselves. This means the new object and the original object will share references to those nested structures.
Modifying a nested object in a shallow copy will still affect the original object, and vice versa, because they both point to the same nested object in memory.
Methods for Shallow Copy
1. Spread Syntax (...)
The spread syntax is a concise and popular way to create shallow copies of both objects and arrays.
// For objects
const originalUser = { id: 1, name: "Bob", settings: { theme: "dark" } };
const shallowCopyUser = { ...originalUser };
shallowCopyUser.name = "Robert"; // Modifies top-level property in copy
shallowCopyUser.settings.theme = "light"; // Modifies nested object, affecting original!
console.log(originalUser.name); // Bob
console.log(shallowCopyUser.name); // Robert
console.log(originalUser.settings.theme); // light (Original affected!)
console.log(shallowCopyUser.settings.theme); // light
// For arrays
const originalNumbers = [1, 2, [3, 4]];
const shallowCopyNumbers = [...originalNumbers];
shallowCopyNumbers[0] = 10; // Modifies top-level primitive
shallowCopyNumbers[2][0] = 30; // Modifies nested array, affecting original!
console.log(originalNumbers[0]); // 1
console.log(shallowCopyNumbers[0]); // 10
console.log(originalNumbers[2]); // [30, 4] (Original affected!)
console.log(shallowCopyNumbers[2]); // [30, 4]
2. Object.assign()
The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the target object. It's often used to create a shallow copy by passing an empty object as the target.
const originalProduct = { id: 'P001', price: 100, details: { weight: '1kg' } };
const shallowCopyProduct = Object.assign({}, originalProduct);
shallowCopyProduct.price = 120; // Modifies top-level property
shallowCopyProduct.details.weight = '1.2kg'; // Modifies nested object, affecting original!
console.log(originalProduct.price); // 100
console.log(shallowCopyProduct.price); // 120
console.log(originalProduct.details.weight); // 1.2kg (Original affected!)
3. Array.prototype.slice()
The slice() method returns a shallow copy of a portion of an array into a new array object. If no arguments are provided, it copies the entire array.
const originalItems = ['apple', { type: 'fruit' }];
const shallowCopyItems = originalItems.slice();
shallowCopyItems[0] = 'orange'; // Modifies top-level primitive
shallowCopyItems[1].type = 'food'; // Modifies nested object, affecting original!
console.log(originalItems[0]); // apple
console.log(shallowCopyItems[0]); // orange
console.log(originalItems[1].type); // food (Original affected!)
4. Array.from()
The Array.from() method creates a new, shallow-copied Array instance from an array-like or iterable object.
const originalData = [10, [20, 30]];
const shallowCopyData = Array.from(originalData);
shallowCopyData[0] = 100; // Modifies top-level primitive
shallowCopyData[1][0] = 200; // Modifies nested array, affecting original!
console.log(originalData[0]); // 10
console.log(shallowCopyData[0]); // 100
console.log(originalData[1]); // [200, 30] (Original affected!)
Deep Copy: Independent Replication
A deep copy creates a completely new object or array, and recursively copies all nested objects and arrays within it. This means that the new object and the original object are entirely independent, with no shared references at any level. Any changes made to the deep copy will not affect the original, and vice versa.
Deep copies are essential when you need to ensure that modifications to a copied object do not inadvertently alter the original data, especially when dealing with complex, multi-level data structures.
Methods for Deep Copy
1. JSON.parse(JSON.stringify(object))
This is a common, simple trick to achieve a deep copy, particularly useful for objects that contain only primitive values, plain objects, and arrays. It works by serializing the object into a JSON string and then parsing it back into a new JavaScript object.
const originalComplexObject = {
name: "Factory",
address: {
street: "Main St",
zip: 12345
},
employees: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
};
const deepCopyComplexObject = JSON.parse(JSON.stringify(originalComplexObject));
deepCopyComplexObject.address.zip = 54321; // Modifies nested object in copy
deepCopyComplexObject.employees[0].name = "Alicia"; // Modifies nested object in array in copy
console.log(originalComplexObject.address.zip); // 12345 (Original unaffected!)
console.log(originalComplexObject.employees[0].name); // Alice (Original unaffected!)
console.log(deepCopyComplexObject.address.zip); // 54321
console.log(deepCopyComplexObject.employees[0].name); // Alicia
Limitations of JSON.parse(JSON.stringify()):
- Loses functions,
undefined, andSymbol: These types are not valid JSON and will be stripped out. - Dates become strings:
Dateobjects are converted to ISO 8601 string format, not actualDateobjects. - Ignores
RegExp,Map,Set,BigInt: These types are not handled correctly;RegExpbecomes an empty object{}, andMap/Setbecome empty objects. - Cannot handle circular references: If an object has a property that refers back to itself (directly or indirectly),
JSON.stringify()will throw an error (TypeError: Converting circular structure to JSON).
const objectWithSpecialTypes = {
num: 1,
str: "hello",
bool: true,
n: null,
u: undefined, // Will be lost
s: Symbol('sym'), // Will be lost
date: new Date(), // Will become string
func: () => console.log('hi'), // Will be lost
reg: /abc/g, // Will become {}
map: new Map([[1, 'a']]), // Will become {}
set: new Set([1, 2]) // Will become {}
};
const deepCopySpecial = JSON.parse(JSON.stringify(objectWithSpecialTypes));
console.log(objectWithSpecialTypes.date); // Date object
console.log(deepCopySpecial.date); // String
console.log(deepCopySpecial.func); // undefined
console.log(deepCopySpecial.reg); // {}
2. The structuredClone() API
Introduced in modern browsers (and Node.js 17+), the structuredClone() global function provides a robust and built-in way to create deep copies. It's designed to handle a wide range of JavaScript built-in types and circular references.
const originalDataWithComplexTypes = {
name: "Report",
creationDate: new Date(),
status: "pending",
config: {
maxRetries: 3,
logLevel: "info"
},
items: [
{ id: 1, value: 100 },
{ id: 2, value: 200 }
],
// structuredClone can handle circular references!
selfRef: null
};
originalDataWithComplexTypes.selfRef = originalDataWithComplexTypes;
const deepCopyData = structuredClone(originalDataWithComplexTypes);
deepCopyData.config.logLevel = "debug";
deepCopyData.items[0].value = 150;
deepCopyData.creationDate.setFullYear(2025); // Modifying Date object directly in copy
console.log(originalDataWithComplexTypes.config.logLevel); // info (Original unaffected!)
console.log(originalDataWithComplexTypes.items[0].value); // 100 (Original unaffected!)
console.log(originalDataWithComplexTypes.creationDate.getFullYear()); // Original year
console.log(deepCopyData.creationDate.getFullYear()); // 2025
// Test circular reference
console.log(deepCopyData.selfRef === deepCopyData); // true (circular ref correctly copied)
console.log(deepCopyData.selfRef === originalDataWithComplexTypes); // false
Advantages of structuredClone():
- Handles many built-in types:
Date,RegExp,Map,Set,ArrayBuffer,Blob,File,ImageData, etc. - Correctly copies circular references without errors.
- More performant than
JSON.parse(JSON.stringify())for complex objects.
Limitations of structuredClone():
- Cannot clone functions.
- Cannot clone DOM nodes.
- Cannot clone error objects (e.g.,
Error,TypeError). - Cannot clone objects with non-transferable properties (e.g.,
WritableStreamDefaultWriter).
structuredClone() is the recommended modern approach for deep copying in most scenarios where its limitations are acceptable.
3. Custom Recursive Functions
For highly specific requirements or older environments, you might need to write a custom recursive deep copy function. This involves iterating through properties, checking their types, and recursively calling the copy function for nested objects/arrays. This approach offers ultimate control but can be complex to implement correctly, especially when dealing with edge cases like circular references or specific object types.
4. Libraries (e.g., Lodash's _.cloneDeep)
For robust, production-ready deep cloning that handles virtually all JavaScript data types (including functions, regexes, and complex objects, and circular references gracefully), utility libraries like Lodash offer battle-tested solutions. _.cloneDeep() from Lodash is a prime example.
// Example with Lodash (assuming Lodash is imported)
// import _ from 'lodash';
// const original = { a: 1, b: { c: 2 }, d: [3, { e: 4 }], f: () => {} };
// const deepCopy = _.cloneDeep(original);
// deepCopy.b.c = 20;
// deepCopy.d[1].e = 40;
// console.log(original.b.c); // 2
// console.log(original.d[1].e); // 4
// console.log(typeof deepCopy.f); // function (functions are typically copied by reference, or skipped, depending on library config)
Libraries are a good choice when you need comprehensive deep cloning capabilities beyond what structuredClone() offers, or when you are already using such a library in your project.
Shallow vs. Deep: When to Use Which?
- Use shallow copy when:
- Your object/array contains only primitive values.
- You intentionally want to share references to nested objects/arrays because you expect them to be mutated together, or they are treated as immutable externally.
- You need to copy a top-level structure quickly and efficiently, and you are sure that modifications to nested objects won't cause issues.
- Use deep copy when:
- Your object/array has nested objects/arrays.
- You need a completely independent copy, ensuring that any modifications to the copy (at any level) will not affect the original data.
- You are working with complex data structures where shared references could lead to difficult-to-debug side effects.
Conclusion
The distinction between shallow and deep copying is fundamental for effective and bug-free JavaScript development, especially when dealing with objects and arrays. While shallow copy methods like the spread syntax or Object.assign() are perfect for top-level duplication or when nested references are acceptable, deep copy is indispensable for creating fully independent duplicates of complex data structures.
For modern applications, structuredClone() stands out as the go-to built-in solution for deep copying, offering robustness and handling many common complex types and circular references. When structuredClone()'s limitations are hit, or for broader compatibility with older environments, leveraging a trusted utility library like Lodash's _.cloneDeep remains a solid and comprehensive option.