Javascript: Deep & Shallow Clone Complete Guide
Luke
Posted on August 28, 2024
Shallow Clone
Shallow clone refers to creating a new object that exactly copies the original object's properties. If the copied value is a primitive data type, the value itself is copied. If it's a reference data type, the memory address is copied. If the memory address of one object changes, the other object will also reflect that change.
(1) Object.assign()
Object.assign() is a method introduced in ES6 for copying objects. The first parameter is the target object, and the remaining parameters are source objects. The syntax is Object.assign(target, source_1, ...). This method can perform shallow copying and can also perform deep copying of one-dimensional objects.
Notes:
If the target object and source objects have properties with the same name, or if multiple source objects have properties with the same name, the properties of the latter objects will overwrite those of the former.
If the function has only one parameter, and that parameter is an object, it returns the object directly. If the parameter is not an object, it converts the parameter to an object and then returns it.
Since null and undefined cannot be converted to objects, the first parameter cannot be null or undefined, or it will throw an error.
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);
console.log(target); // {a: 1, b: 2, c: 3}
(2) Spread Operator
The spread operator can be used to copy properties when constructing literal objects. The syntax is: let cloneObj = { ...obj };.
let obj1 = {a: 1, b: {c: 1}};
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); // {a: 2, b: {c: 1}}
console.log(obj2); // {a: 1, b: {c: 1}}
obj1.b.c = 2;
console.log(obj1); // {a: 2, b: {c: 2}}
console.log(obj2); // {a: 1, b: {c: 2}}
(3) Array Methods for Shallow Copying Arrays
Array.prototype.slice
The slice() method is a JavaScript array method that returns selected elements from an existing array. The syntax is array.slice(start, end), and the method does not modify the original array.
The method takes two optional parameters. If neither is provided, the method creates a shallow copy of the array.
let arr = [1, 2, 3, 4];
console.log(arr.slice()); // [1, 2, 3, 4]
console.log(arr.slice() === arr); // false
Array.prototype.concat
The concat() method is used to merge two or more arrays. This method does not modify the existing arrays but returns a new array.
The method takes two optional parameters. If neither is provided, the method creates a shallow copy of the array.
let arr = [1, 2, 3, 4];
console.log(arr.concat()); // [1, 2, 3, 4]
console.log(arr.concat() === arr); // false
(4) Implementing Shallow Clone
Here is a JavaScript code example to manually implement a shallow copy:
// Implementation of shallow copy
function shallowClone(object) {
// Only copy objects
if (object === null || typeof object !== "object") return object;
// Determine whether to create a new array or object based on the type of the original object
let newObject = Array.isArray(object) ? [] : {};
// Iterate over the original object, and only copy its own properties
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
Explanation:
Type Checking: The function first checks if the input is an object (not null or a primitive type). If not, it returns itself.
Type Determination: Based on whether the original object is an array or a plain object, the function creates a new array or a new object to hold the copied properties.
Property Copying: The function then iterates over the original object using a for...in loop. It checks if the property is directly on the object (i.e., not inherited) using hasOwnProperty(). If so, it copies the property to the new object.
Return: Finally, the new object (or array) with the copied properties is returned.
Test Case:
const original = {
name: 'Alice',
age: 25,
details: {
city: 'New York'
},
hobbies: ['reading', 'swimming']
};
const copy = shallowClone(original);
console.log(copy);
// Output: { name: 'Alice', age: 25, details: { city: 'New York' }, hobbies: ['reading', 'swimming'] }
copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.hobbies[0] = 'running';
console.log(original.name); // 'Alice', the basic property of the original object is not modified
console.log(original.details.city); // 'Los Angeles', the reference of the nested object is shared
console.log(original.hobbies[0]); // 'running', the reference of the array elements is shared
Deep Clone
Deep clone, in contrast to shallow clone, creates a new reference type and copies the corresponding values when encountering properties that are reference types. This means the object obtains a new reference type rather than a reference to the original type. Deep clone can be achieved for some objects using JSON's two functions, but since the JSON format is stricter than the JavaScript object format, it may fail when the property values include functions or Symbol types.
(1) JSON.stringify()
JSON.parse(JSON.stringify(obj)) is one of the more commonly used methods for deep copying. Its principle is to use JSON.stringify to serialize the JavaScript object into a JSON string, and then use JSON.parse to deserialize (restore) the JavaScript object.
This method can achieve deep copying in a simple and straightforward manner, but it has some limitations. If the object being copied contains functions, undefined, Date, RegExp, Map, Set, or Symbol values, these will be lost after processing with JSON.stringify().
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
const original = {
name: 'Alice',
age: 25,
details: {
city: 'New York',
hobbies: ['reading', 'swimming']
}
};
const copy = deepClone(original);
copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.details.hobbies[0] = 'running';
console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.details.hobbies[0]); // 'reading'
(2)_.cloneDeep provided by Lodash
The lodash library provides the _.cloneDeep method for performing deep copies.
const _ = require('lodash');
const original = {
name: 'Alice',
age: 25,
details: {
city: 'New York',
hobbies: ['reading', 'swimming'],
birthDate: new Date('1995-01-01')
}
};
const copy = _.cloneDeep(original);
copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.details.hobbies[0] = 'running';
copy.details.birthDate.setFullYear(2000);
console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.details.hobbies[0]); // 'reading'
console.log(original.details.birthDate.getFullYear()); // 1995
(3) structuredClone() global function
structuredClone() is a native JavaScript API used for deep copying. It can clone a JavaScript object and recursively copy all of its nested child objects, rather than just copying references. Compared to traditional deep copy methods, structuredClone() is more powerful and flexible.
Features of structuredClone()
Deep Copy: structuredClone() recursively copies an object and all of its nested child objects and arrays, creating a completely new object copy. Modifying the copy does not affect the original object, and vice versa.
Support for Multiple Data Types: structuredClone() can correctly handle most JavaScript data types, including:
Primitive data types (e.g., strings, numbers, booleans)
Objects and arrays
Date objects
RegExp objects
Map and Set
ArrayBuffer, TypedArray, Blob, File, FileList, and other types
Unsupported Types: structuredClone() cannot clone the following types:
Functions (Function)
Symbol
Objects with circular references (this will throw an error)
Performance Optimization: Since structuredClone() is natively implemented by the browser, it generally offers better performance and stability compared to manually implemented deep copy methods.
const original = {
name: 'Alice',
age: 25,
details: {
city: 'New York',
birthDate: new Date('1995-01-01'),
pattern: /abc/g,
},
hobbies: ['reading', 'swimming'],
nestedArray: [1, [2, 3]],
};
// Perform deep copy using structuredClone
const copy = structuredClone(original);
// Modify the copy
copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.hobbies[0] = 'running';
copy.nestedArray[1][0] = 99;
console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.hobbies[0]); // 'reading'
console.log(original.nestedArray[1][0]); // 2
console.log(copy.name); // 'Bob'
console.log(copy.details.city); // 'Los Angeles'
console.log(copy.hobbies[0]); // 'running'
console.log(copy.nestedArray[1][0]); // 99
structuredClone() is particularly well-suited for the following scenarios:
Deep Copying Complex Data Structures: When you need to copy an object that contains nested objects, arrays, or other complex structures, structuredClone() ensures complete independence between the copy and the original object.
Avoiding the Complexity of Manual Deep Copying: Manually implementing deep copying requires handling various edge cases and data types. structuredClone() provides a native solution that handles these situations more simply and reliably.
(4) Implementing Deep Clone
function deepClone(object, hash = new WeakMap()) {
// Non-object types or null, return directly
if (object === null || typeof object !== "object") return object;
// Check for circular references
if (hash.has(object)) return hash.get(object);
// Handle special object types
let newObject;
if (Array.isArray(object)) {
newObject = [];
} else if (object instanceof Date) {
newObject = new Date(object);
} else if (object instanceof RegExp) {
newObject = new RegExp(object);
} else {
newObject = {};
}
// Record the current object to avoid circular references
hash.set(object, newObject);
// Recursively copy all properties
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = deepClone(object[key], hash);
}
}
return newObject;
}
// Test example
const original = {
name: "Alice",
age: 25,
details: {
city: "New York",
hobbies: ["reading", "swimming"],
birthDate: new Date("1995-01-01"),
pattern: /abc/g,
},
};
// Introduce a circular reference
original.self = original;
const copy = deepCopy(original);
console.log(copy);
Notice:
Return value for non-object types: If object is null or a non-object type (such as string, number, boolean), the function directly returns object. This ensures that the deep copy function returns the original value instead of undefined when encountering non-object types.
Handling circular references: A WeakMap is used to track objects that have already been copied. If the same object is encountered again, the function returns the previously created copy to prevent infinite recursion.
Handling special object types: The function includes special handling for Date and RegExp objects to correctly copy instances of these types. If additional types (e.g., Map, Set) need to be supported, the code can be further extended based on specific requirements.
Posted on August 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 25, 2024
October 6, 2024