When working with JavaScript, especially with objects and arrays, you’ll soon encounter a crucial but often confusing concept: data copying. Accidentally modifying an object without realizing it can lead to hard-to-find bugs that take hours to fix.
The main culprit behind these issues is the difference between Shallow Copy and Deep Copy.
This article will help you master these two concepts, so you can write cleaner, more predictable code and avoid those “out of nowhere” bugs. Let’s get started! 🚀
Core Foundation: Primitive vs Reference Types
Before diving into shallow and deep copy, let’s understand how JavaScript handles data types.
1. Primitive Types
Primitive types like string
, number
, boolean
, null
, undefined
, symbol
, and bigint
store their actual values directly.
When you assign a primitive variable to another, you’re copying the value. They are completely independent.
let a = 100
let b = a // Copy value of a to b
console.log(b) // 100
// Changing b does not affect a
b = 200
console.log(a) // 100 (Unchanged)
console.log(b) // 200
2. Reference Types
Types like Object
, Array
, and Function
do not store values directly. Instead, the variable holds a reference or address pointing to the object’s location in memory.
💡 Analogy: Imagine a variable as a piece of paper.
- With primitive, the paper has the value written on it ("100").
- With reference, the paper has the address of a house (
0x123...
), and the actual data is inside that house.
When you assign a reference variable to another, you’re just copying the address, not building a new house. Both variables now point to the same house.
let user1 = { name: 'Alice', age: 25 }
let user2 = user1 // Copy memory address of user1 to user2
// Change data via user2
user2.name = 'Bob'
// user1 is also changed!
console.log(user1.name) // "Bob" 😱
This is the root of many issues and why we need copying techniques.
Shallow Copy: Only Copies the Surface 🌊
A shallow copy creates a new object or array, then copies properties from the original to the new one.
- If a property is a primitive, its value is copied.
- If a property is a reference (an object or array inside), only the reference (address) is copied, not the actual nested object.
In other words, the new and original objects share nested objects/arrays.
Ways to Perform Shallow Copy
1. Spread Syntax (...
)
This is the most popular and modern way for both objects and arrays.
const original = {
name: 'Laptop',
price: 1000,
details: {
// This is a nested object
brand: 'Apple',
year: 2023,
},
}
const copy = { ...original }
// Change top-level property (primitive)
copy.price = 1200
console.log(original.price) // 1000 (Not affected ✅)
// Change nested object property
copy.details.brand = 'Dell'
console.log(original.details.brand) // "Dell" (Affected 😱)
As you can see, original
and copy
share the details
object. Changing one affects the other.
2. Object.assign()
Works similarly to Spread Syntax for objects.
const original = { name: 'Alice', address: { city: 'Hanoi' } }
const copy = Object.assign({}, original)
copy.address.city = 'Da Nang'
console.log(original.address.city) // "Da Nang" (Affected 😱)
3. Array.prototype.slice()
and Array.from()
Common methods for shallow copying arrays.
const originalArray = [1, 2, [3, 4]]
const copiedArray = originalArray.slice()
// Change nested array
copiedArray[2][0] = 99
console.log(originalArray[2][0]) // 99 (Affected 😱)
When to use Shallow Copy? When your object/array is flat (no nested elements) or when you intentionally want to share nested objects to save memory or synchronize data.
Deep Copy: Creating a Fully Independent Copy 🧱
A deep copy creates a new object/array and recursively copies all data. Every nested object and array is also copied, creating completely new instances.
The result: two objects/arrays are fully independent, with no shared references. Changes to the copy never affect the original.
Ways to Perform Deep Copy
1. The JSON Trick: JSON.stringify()
and JSON.parse()
This is the simplest and fastest way for objects containing only valid JSON data (string, number, boolean, array, object).
const original = {
name: 'Laptop',
price: 1000,
details: {
brand: 'Apple',
year: 2023,
},
buy: function () {
console.log('Bought!')
},
}
const deepCopy = JSON.parse(JSON.stringify(original))
// Change nested object property
deepCopy.details.brand = 'Dell'
console.log(original.details.brand) // "Apple" (Not affected ✅ Great!)
⚠️ Important caveats of the JSON trick:
- Data loss: Properties with values of
function
,undefined
, orSymbol
are ignored. - Incorrect data:
Date
objects become strings.NaN
,Infinity
becomenull
. - Cannot handle circular references.
Only use this method when you’re sure your object is simple and JSON-compatible.
2. The structuredClone()
API (Modern Approach)
A new built-in API in browsers and Node.js, designed specifically for deep copying. Much more powerful than the JSON trick.
const original = {
name: 'Complex Object',
date: new Date(),
nested: { set: new Set([1, 2, 3]) },
}
const deepCopy = structuredClone(original)
deepCopy.nested.set.add(4)
console.log(original.nested.set) // Set(3) { 1, 2, 3 } (Not affected ✅)
console.log(deepCopy.nested.set) // Set(4) { 1, 2, 3, 4 }
structuredClone()
can handle more complex types like Date
, RegExp
, Set
, Map
, ArrayBuffer
, etc. However, it cannot copy functions and will throw an error for circular references.
3. Using External Libraries (Most Reliable)
For complex applications, the safest and most reliable way is to use well-tested functions from popular libraries like Lodash.
Lodash’s cloneDeep
can handle almost any case, including functions, complex types, and circular references.
// Install lodash: npm install lodash
import _ from 'lodash'
const original = {
name: 'Super Object',
run: () => console.log('Running!'),
details: { brand: 'BrandX' },
}
const deepCopy = _.cloneDeep(original)
deepCopy.details.brand = 'BrandY'
console.log(original.details.brand) // "BrandX" (Not affected ✅)
console.log(typeof original.run) // "function"
console.log(typeof deepCopy.run) // "function" (Function is also copied ✅)
Conclusion: Choose the Right Tool for the Job
Understanding the difference between Shallow Copy and Deep Copy is not just theory—it’s an essential skill for writing robust, maintainable JavaScript code.
Criteria | Shallow Copy | Deep Copy |
---|---|---|
Definition | Creates new object/array, copies top-level props. | Creates new object/array, recursively copies all properties. |
Nested objects | Shares references with the original. | Nested objects are copied as new, fully independent objects. |
Independence | Partially independent (not for nested levels). | Fully independent. |
Speed | Faster. | Slower due to traversing the whole structure. |
Methods | ... , Object.assign , slice() | structuredClone() , JSON.parse/stringify , _.cloneDeep() (Lodash) |
When to use | Flat data, no nesting. Need performance. | Complex data. Need absolute independence. |
- Shallow Copy is fast and efficient for simple data structures. Use Spread (
...
) as your default choice. - Deep Copy is your savior when you need complete data independence and want to avoid unwanted side effects.
structuredClone()
is a modern, powerful choice, while Lodash’s_.cloneDeep()
is the most comprehensive solution for complex cases.
Hopefully, this article has given you a clear and deep understanding of these two important concepts. Now you can be more confident in managing and manipulating data in your projects!