Introduction
When working with JavaScript, understanding the difference between shallow copy and deep copy is essential for handling object replication correctly. Before diving into these copying mechanisms, let's first examine the two categories of data types in JavaScript:
- Primitive Types (7 types): String, Number, Boolean, null, undefined, Symbol, BigInt
- Reference Types: Object (including Array, Function, RegExp, Date, etc.)
How Data is Stored in Memory
Primitive types are stored directly in the stack as simple data segments. They can be accessed and manipulated by their direct values. Reference types, how ever, are stored in the heap memory. The stack contains a pointer (reference address) that points to the actual object in heap memory. This pointer allows JavaScript to locate and access the object efficiently.
Shallow Copy
What is a Shallow Copy?
A shallow copy creates a new data structure that contains references to the same memory addresses as the original object for nested reference types. In other words:
- For primitive types, the actual value is copied
- For reference types, only the memory address is copied, meaning both the original and copied objects share the same nested objects
Methods to Create Shallow Copies
1. Using Object.assign()
const source = {
age: 25,
profile: {
firstName: 'alice',
lastName: 'bob'
},
tags: ['developer', 'engineer'],
greet: function() {
console.log('hello');
}
};
const target = Object.assign({}, source);
// Since it's a shallow copy, only primitive values are truly copied
// Reference types still share the same memory address
source.profile.firstName = 'changed';
source.tags[0] = 'modified';
console.log(target.profile.firstName); // Output: changed
2. Using Array.slice()
const originalArray = ["First", {
title: "Second",
version: 3
}, "Third"];
const copiedArray = originalArray.slice(0);
originalArray[1].title = "Fourth";
console.log(copiedArray[1].title); // Output: Fourth
3. Using Array.concat()
const originalArray = ["First", {
title: "Second",
version: 3
}, "Third"];
const copiedArray = originalArray.concat();
originalArray[1].title = "Fourth";
console.log(copiedArray[1].title); // Output: Fourth
4. Using Spread Operator
const originalArray = ["First", {
title: "Second",
version: 3
}, "Third"];
const copiedArray = [...originalArray];
originalArray[1].title = "Fourth";
console.log(copiedArray[1].title); // Output: Fourth
Deep Copy
What is a Deep Copy?
A deep copy creates a completely independent copy of an object, including all nested objects. Changes made to the copied object will not affect the original object, and vice versa. This is achieved by allocating new memory space for all properties, including nested reference types.
Methods to Create Deep Copies
1. Using JSON.parse() and JSON.stringify()
const source = {
age: 25,
profile: {
firstName: 'alice',
lastName: 'bob'
},
tags: ['developer', 'engineer'],
greet: function() {
console.log('hello');
}
};
const target = JSON.parse(JSON.stringify(source));
source.profile.firstName = 'modified';
console.log(target.profile.firstName); // Output: alice
Important Limitations:
- Properties with
undefinedvalues are ignored - Function properties are ignored
- Symbol keys are not preserved
- Non-circular references are handled correctly
- Performance may degrade with large data structures
Custom Implementation of Shallow and Deep Copy
Custom Shallow Copy Function
function shallowCopy(original) {
const result = {};
for (const property in original) {
if (Object.prototype.hasOwnProperty.call(original, property)) {
result[property] = original[property];
}
}
return result;
}
Testing the shallow copy:
const source = {
age: 25,
profile: {
firstName: 'alice',
lastName: 'bob'
},
tags: ['developer', 'engineer'],
greet: function() {
console.log('hello');
}
};
const copy = shallowCopy(source);
console.log(source);
console.log(copy);
Custom Deep Copy Function
function deepClone(original, cache = new WeakMap()) {
// Handle null and undefined
if (original === null) return original;
// Handle Date objects
if (original instanceof Date) {
return new Date(original.getTime());
}
// Handle RegExp objects
if (original instanceof RegExp) {
return new RegExp(original.source, original.flags);
}
// For primitive types and functions, return as-is
if (typeof original !== 'object') {
return original;
}
// Check cache to handle circular references
if (cache.get(original)) {
return cache.get(original);
}
// Create new object of the same type
const cloneTarget = new original.constructor();
cache.set(original, cloneTarget);
// Recursively copy all properties
for (const key in original) {
if (Object.prototype.hasOwnProperty.call(original, key)) {
cloneTarget[key] = deepClone(original[key], cache);
}
}
return cloneTarget;
}
Testing the deep copy:
const source = {
age: 25,
profile: {
firstName: 'alice',
lastName: 'bob'
},
tags: ['developer', 'engineer'],
greet: function() {
console.log('hello');
}
};
const deepCopyResult = deepClone(source);
const shallowCopyResult = Object.assign({}, source);
const customShallowCopy = shallowCopy(source);
source.profile.firstName = 'modified';
// Deep copy creates new memory, so original change doesn't affect the copy
console.log(deepCopyResult.profile.firstName); // Output: alice
// Shallow copies share reference types with the original
console.log(shallowCopyResult.profile.firstName); // Output: modified
console.log(customShallowCopy.profile.firstName); // Output: modified