Shallow Copy vs. Deep Copy in JavaScript
In this article, we'll explore the differences between shallow and deep copy in JavaScript and when to use each technique.
Join the DZone community and get the full member experience.
Join For FreeAs a JavaScript developer, you must be familiar with the concept of copying, which is the process of creating a new object or array with the same values as an existing one.
Copying is an essential technique in programming that helps you to avoid unexpected mutations to objects and arrays. Two main techniques for copying in JavaScript are shallow copy and deep copy.
In this article, we'll explore the differences between shallow and deep copy in JavaScript and when to use each technique. We'll also discuss the benefits and limitations of each approach and provide you with best practices to help you avoid common pitfalls.
By the end of this post, you'll better understand how to use the shallow and deep copy in your code effectively. So, let's dive in!
Shallow Copy in JavaScript
JavaScript provides multiple ways to create shallow copies of objects and arrays.
When you make a shallow copy of an object or array, you create a new one that points to the same values as the original. The shallow copy will also change. Therefore, if you change the original object or array. Reversely, if you change the shallow copy, the original object or array will also be affected.
A shallow copy does not clone nested objects, meaning that if the original object contains other objects or arrays as properties, the shallow copy will point to the same references as the original.
Spread Operator
One common way to create a shallow copy of an object or an array in JavaScript is to use the spread operator (…
). The spread operator copies all enumerable properties of an object or an array into a new one. For example:
// Create an original object
let person = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'writing', 'coding'],
}
// Create a shallow copy using the spread operator
let personCopy = { ...person }
// Modify the original object
person.name = 'Bob'
// Check the shallow copy
console.log(personCopy.name) // Alice
console.log(personCopy.hobbies) // ["reading", "writing", "coding"]
// Modify the shallow copy
personCopy.hobbies.push('gaming')
// Check the original object
console.log(person.hobbies) // ["reading", "writing", "coding", "gaming"]
As you can see, the spread operator makes a copy of an object and its properties; however, it doesn't create a copy of nested arrays within the object. Therefore, when we modify the hobbies
property of either object, the change affects both objects.
Object.assign()
Another way to create a shallow copy of an object is to use the Object.assign()
method. It copies all the enumerable and own properties of one or more source objects into a target object. For instance:
// Create an original object
let person = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'writing', 'coding'],
}
// Create a shallow copy using Object.assign()
let personCopy = Object.assign({}, person)
// Modify the original object
person.name = 'Bob'
// Check the shallow copy
console.log(personCopy.name) // Alice
console.log(personCopy.hobbies) // ["reading", "writing", "coding"]
// Modify the shallow copy
personCopy.hobbies.push('gaming')
// Check the original object
console.log(person.hobbies) // ["reading", "writing", "coding", "gaming"]
The Object.assign()
method works similarly to the spread operator, except that it can take multiple source objects as arguments and merge them into the target object. However, it also does not clone nested objects, so modifying them will affect both objects.
Array.slice()
To create a shallow copy of an array, you can use the Array.slice()
method. This method returns a new array with a portion of the original array. If no arguments are provided, it returns a copy of the whole array.
The Array.slice()
method creates a new array with the same elements as the original array, but it does not clone nested arrays or objects. Therefore, modifying them will affect both arrays. Here's an example:
// Create an original array with a nested array and an object as elements
let originalArray = [[1, 2, 3], {name: "Alice", age: 25}];
// Create a new array using Array.slice()
let newArray = originalArray.slice();
// Check the values and references of the original array and the new array
console.log(originalArray); // [[1, 2, 3], {name: "Alice", age: 25}]
console.log(newArray); // [[1, 2, 3], {name: "Alice", age: 25}]
console.log(originalArray[0] === newArray[0]); // true
console.log(originalArray[1] === newArray[1]); // true
// Modify the nested array and the object in the original array
originalArray[0].push(4);
originalArray[1].name = "Bob";
// Check the values and references of the original array and the new array
console.log(originalArray); // [[1, 2, 3, 4], {name: "Bob", age: 25}]
console.log(newArray); // [[1, 2, 3, 4], {name: "Bob", age: 25}]
console.log(originalArray[0] === newArray[0]); // true
console.log(originalArray[1] === newArray[1]); // true
Deep Copy in JavaScript
Creating a deep copy of an object or an array in JavaScript involves duplicating the original object's properties and values, including any nested objects or arrays, into a new object or array without any shared references. This means that changes made to one object or array will not affect the other.
There are various ways to create a deep copy:
JSON.parse(JSON.stringify())
One simple method is using the JSON.parse(JSON.stringify())
method, which converts an object or array into a JSON string and then parses it back into a new object or array. For instance:
// Create an original object
let person = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'writing', 'coding'],
}
// Create a deep copy using JSON.parse(JSON.stringify())
let personCopy = JSON.parse(JSON.stringify(person))
// Modify the original object
person.name = 'Bob'
// Check the deep copy
console.log(personCopy.name) // Alice
console.log(personCopy.hobbies) // ["reading", "writing", "coding"]
// Modify the deep copy
personCopy.hobbies.push('gaming')
// Check the original object
console.log(person.hobbies) // ["reading", "writing", "coding"]
As you can see, the JSON.parse(JSON.stringify())
method creates a new object with the same values and properties as the original object but also clones the nested array. Therefore, when we modify the hobbies property of either object, the change does not affect the other object.
lodash.cloneDeep()
Another way is to use a third-party library such as lodash
, which provides the cloneDeep()
method that recursively clones an object or an array and any nested objects or arrays. For example:
// Import lodash.cloneDeep
import cloneDeep from 'lodash/cloneDeep'
// Create an original object
let person = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'writing', 'coding'],
}
// Create a deep copy using lodash.cloneDeep()
let personCopy = cloneDeep(person)
// Modify the original object
person.name = 'Bob'
// Check the deep copy
console.log(personCopy.name) // Alice
console.log(personCopy.hobbies) // ["reading", "writing", "coding"]
// Modify the deep copy
personCopy.hobbies.push('gaming')
// Check the original object
console.log(person.hobbies) // ["reading", "writing", "coding"]
The lodash.cloneDeep()
function works similarly to the JSON.parse(JSON.stringify())
method, except that it does not rely on JSON serialization and deserialization. It also does not clone functions or symbols, so it may not work for some complex objects.
However, unlike JSON.parse(JSON.stringify())
,lodash.cloneDeep()
can handle circular references and undefined values, and it can clone other data types or methods that JSON cannot serialize, such as dates, regexes, maps, sets, etc., by using customizer functions or plugins.
Recursive Function
Alternatively, you can write a custom recursive function that iterates over the properties of an object or an array and creates new copies of them. For instance:
// Create a custom function for deep copying
function deepCopy(obj) {
// Check if obj is an array or an object
if (Array.isArray(obj)) {
// Create a new array and loop over its elements
let newArr = []
for (let i = 0; i < obj.length; i++) {
// Recursively call deepCopy on each element and push it to newArr
newArr.push(deepCopy(obj[i]))
}
// Return newArr
return newArr
} else if (typeof obj === 'object' && obj !== null) {
// Create a new object and loop over its keys
let newObj = {}
for (let key in obj) {
// Recursively call deepCopy on each value and assign it to newObj[key]
newObj[key] = deepCopy(obj[key])
}
// Return newObj
return newObj
} else {
// Return obj as it is (primitive value)
return obj
}
}
// Create an original object
let person = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'writing', 'coding'],
}
// Create a deep copy using deepCopy()
let personCopy = deepCopy(person)
// Modify the original object
person.name = 'Bob'
// Check the deep copy
console.log(personCopy.name) // Alice
console.log(personCopy.hobbies) // ["reading", "writing", "coding"]
// Modify the deep copy
personCopy.hobbies.push('gaming')
// Check the original object
console.log(person.hobbies) // ["reading", "writing", "coding"]
You can see that the deepCopy()
function returns a new object with the same values and properties as the original object but also clones the nested array. Therefore, when we modify the hobbies
property of either object, the change does not affect the other object.
Comparing Shallow Copy vs. Deep Copy in JavaScript
Now that we have a good understanding of what shallow copy and deep copy are let's take a closer look at the differences between them.
Key Difference | Shallow Copy | Deep Copy |
---|---|---|
Level of Copying | Copies only top-level properties or elements | Creates a complete and independent copy of the entire object or array, including all nested objects or arrays |
Reference Handling | Copies references to nested objects or arrays | Creates new instances of all nested objects or arrays |
Performance | Less expensive in terms of performance and memory usage | More expensive in terms of performance and memory usage |
Circular References | Handles circular references better | Creates circular references in the copy |
Level of Copying
The primary difference between shallow copy and deep copy is the level of copying they perform. Shallow copy only copies the top-level properties or elements of an object or array, while deep copy creates a complete and independent copy of the entire object or array, including all nested objects or arrays.
Reference Handling
Another significant difference between shallow copy and deep copy is how they handle references. Shallow copy only copies references to nested objects or arrays, while deep copy creates new instances of all nested objects or arrays.
Performance
A deep copy can be more expensive in terms of performance and memory usage than a shallow copy. This is because deep copy requires more processing power and memory to create and store a complete copy of the original object or array.
Circular References
A shallow copy can handle circular references better than a deep copy. For example, when copying an object or array with a circular reference, a shallow copy will copy the reference to the original object or array, while a deep copy will also create a circular reference in the copy.
Pros and Cons of Shallow Copy
Here are some of the pros and cons of using shallow copy in JavaScript:
Pros
- Saves Memory: Shallow copy can be useful in cases where you want to save memory by avoiding the creation of a completely new object or array. By copying only the top-level properties or elements of the original object or array, you can save memory without affecting the original object or array.
- Quick and Simple: Shallow copy is generally faster and simpler than deep copy since it only copies the top-level properties or elements of an object or array. This can be useful in cases where speed and simplicity are important.
- Preserves References: Shallow copy preserves references to the original object or array, which can be useful in certain scenarios where you want to maintain a connection between the original and copied objects or arrays.
Cons
- Incomplete Copy: Shallow copy only copies the top-level properties or elements of an object or array. This means that any nested objects or arrays are not copied, but instead, their references are copied. This can lead to unintended side effects or changes to the original object or array.
- Potential for Overwriting: Shallow copy can also lead to the potential for overwriting the original object or array, especially when working with mutable objects or arrays. Any changes made to the shallow copy can also affect the original object or array.
- Limited Use Cases: Shallow copy is best suited for simple objects or arrays with only top-level properties or elements. For more complex data structures, such as nested objects and arrays or objects with circular references, a shallow copy may not be sufficient.
Pros and Cons of Deep Copy
Here are the pros and cons of using deep copy in JavaScript:
Pros
Some of the pros of using deep copy in JavaScript are:
- Creates Independent Copies: Deep copy creates completely independent copies of an object or array, which can be useful in cases where you want to avoid unintended side effects or changes to the original object or array.
- More Accurate: Deep copy is generally more accurate than shallow copy since it creates a true copy of all nested objects and arrays. This can be important in cases where accuracy is critical.
- Can Handle Complex Objects and Arrays: Deep copy is designed to handle complex objects and arrays, even those with circular references or nested objects and arrays. This makes it useful in scenarios where you're working with large or complex data structures.
Cons
Some of the cons of using deep copy in JavaScript are:
- Slower Performance: Deep copy can be slower and more resource-intensive than shallow copy, especially for large or complex data structures. This can impact the overall performance of your code.
- Increased Memory Usage: Deep copy can also lead to increased memory usage since it creates completely independent copies of all nested objects and arrays. This can be a concern for memory-intensive applications.
- Difficulty Handling Circular References: While the deep copy is designed to handle circular references, it can still be a challenge to implement in certain scenarios. This can lead to errors or unexpected behavior in your code.
Use Cases for Shallow Copy and Deep Copy
Shallow copy and deep copy have different use cases depending on the specific needs of your code. Here are some common scenarios where each method may be more appropriate:
When to Use Shallow Copy
- When you need to create a new object or array that shares some of the properties or elements of an existing object or array, you want to avoid affecting the original object or array.
- When you need to pass an object or array as a parameter to a function and want to avoid unintended side effects by creating a copy of the object or array.
- When you need to quickly create a shallow copy of a small and simple object or array.
When to Use Deep Copy
- When you need to create a completely independent copy of an object or array without any shared references to the original object or array.
- When you need to change a copied object or array without affecting the original object or array.
- When you need to create a deep copy of a large or complex object or array, even if it takes longer than a shallow copy.
Common Pitfalls and Best Practices
While working with shallow copy and deep copy in JavaScript, there are a few common pitfalls to be aware of and some best practices to follow.
Modifying Original Objects or Arrays
One common pitfall when working with shallow and deep copies is modifying the original object or array after creating a copy. If you modify the original object or array, the changes will also be reflected in the copy, which can lead to unexpected behavior.
To avoid this, you should always make a copy of an object or array before modifying it and then work with the copy instead of the original.
Performance Considerations
As mentioned earlier, the deep copy can be more expensive in terms of performance and memory usage than the shallow copy. If you need to create a deep copy of a large or complex object or array, it can take a significant amount of time and memory.
To optimize performance, you should only use deep copy when necessary and try to find alternative solutions when possible.
Choosing the Right Copying Method
Choosing the right copying method is crucial when working with objects or arrays in JavaScript. If you only need to create a copy of the top-level properties or elements, then shallow copy is the way to go. On the other hand, if you need to create a complete and independent copy of an object or array, then deep copy is the better choice.
Using Libraries and Frameworks
There are many libraries and frameworks available that provide built-in methods for copying objects and arrays in JavaScript. These libraries and frameworks can save you time and effort when working with complex objects or arrays.
Some popular libraries and frameworks for copying objects and arrays in JavaScript include Lodash and Ramda.
Conclusion
In JavaScript, shallow copy and deep copy are two commonly used techniques for copying objects and arrays. While both methods can be useful in different scenarios, they have their own advantages and disadvantages.
Shallow copy is a faster and more memory-efficient method for copying objects and arrays. It only creates a new reference to the original object or array, and any changes made to the shallow copy will also affect the original. This makes it a good option for simple data structures with only top-level properties or elements.
On the other hand, deep copy creates a completely independent copy of the original object or array, including any nested objects and arrays. While this method is slower and more memory-intensive than shallow copy, it provides a true copy of the original and can be a better option for more complex data structures.
When deciding which method to use, it's important to consider the specific needs of your application and data structures. In some cases, a combination of shallow copy and deep copy may be the best solution.
Overall, understanding the differences between shallow copy and deep copy is an important part of writing efficient and effective JavaScript code. By taking the time to carefully consider your options, you can ensure that your code is both performant and easy to maintain.
FAQs
Q: What is the difference between shallow copy and deep copy?
A: Shallow copy creates a new reference to the original object or array, while deep copy creates a completely independent copy of the original, including any nested objects or arrays.
Q: When should I use shallow copy?
A: Shallow copy is a good option for simple data structures with only top-level properties or elements. It is also a faster and more memory-efficient option.
Q: When should I use deep copy?
A: Deep copy is a better option for more complex data structures, as it creates a true copy of the original, including any nested objects or arrays.
Q: How do I implement shallow copy and deep copy in JavaScript?
A: Shallow copy can be achieved using Object.assign()
or the spread operator (...
), while deep copy can be achieved using JSON.parse()
and JSON.stringify()
or a custom function that recursively copies the entire object or array.
Q: Are there any performance considerations when using shallow and deep copies?
A: Yes, shallow copy is generally faster and more memory-efficient than deep copy but may not be suitable for all data structures. It's important to consider the specific needs of your application when deciding which method to use.
Q: Can I use a combination of shallow copy and deep copy in my code?
A: Yes, a combination of shallow copy and deep copy may be the best solution in some cases, depending on the specific needs of your application and data structures.
Published at DZone with permission of Tien Nguyen. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments