This article summarizes some common methods for solving deep copy. There is no silver bullet, each method has its pros and cons, and should be distinguished based on its use cases.
In JavaScript, deep copy has been a well-known problem for a long time. It has also been used by interviewers countless times to assess a front-end developer's JavaScript skills. Insufficient understanding of deep copy can lead to hard-to-find bugs in development. Previously, I wrote an article "In-depth Understanding of JavaScript Cloning", which analyzed the concept of deep copy, but its solution was still quite shallow. This article records some common methods for solving deep copy. There is no silver bullet, each method has its pros and cons, and should be distinguished based on its use cases.
Recursive Copy#
Recursive copy is probably the first solution encountered when learning about deep copy. Its principle is to traverse reference types and determine whether their values are reference types. If they are reference types, the function is recursively called with the value as the argument. Otherwise, if they are simple data types, a regular assignment copy is performed.
function deepClone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
}
This is a simple deep copy method that handles objects, arrays, and functions. However, it does not handle reference types such as Date, Regexp, and Map, nor can it solve the scenario of object circular reference.
JSON.parse(JSON.stringify(object))#
In general, deep copy can be solved by using JSON.parse(JSON.stringify(object)).
Limitations of this method:
- It ignores undefined.
- It cannot serialize functions.
- It cannot solve circular reference objects.
Undefined and functions will be ignored, and attempting to copy an object with circular reference will result in an error:
Uncaught TypeError: Converting circular structure to JSON
In addition, Map, Set, RegExp, Date, ArrayBuffer, and other built-in types will be lost during serialization.
However, in most cases, complex data can be serialized, so this function can solve most problems, and it is the fastest deep copy function among built-in functions in terms of performance.
Structured Clone Algorithm#
Structured cloning is an existing algorithm used to transfer values from one place to another. For example, it is used whenever you call postMessage to send a message to another window or WebWorker. The advantage of structured cloning is that it handles circular objects and supports a large number of built-in types. Structured cloning includes the use of MessageChannel and History API.
MessageChannel#
The MessageChannel API allows us to create a new message channel and send data through its two MessagePort properties.
We can create a MessageChannel and send a message. On the receiving end, the message contains a structured clone of our original data object.
The data passed through the postMessage of MessageChannel is also deep copied, just like the postMessage of web worker. It can also copy undefined and objects with circular reference.
// With undefined + circular reference
let obj = {
a: 1,
b: {
c: 2,
d: 3
},
f: undefined
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
function deepCopy(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
// MessageChannel is asynchronous
deepCopy(obj).then(copy => {
let copyObj = copy;
console.log(copyObj, obj);
console.log(copyObj == obj);
});
However, when copying an object with functions, an error will still occur:
Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned.
The disadvantage of this method is that it is asynchronous, but you can use await to solve it.
const clone = await structuralClone(obj);
History#
If you have ever used history.pushState() to write a single-page application (SPA), you can provide a state object to save the URL. It turns out that this state object uses structured cloning and is synchronous. We must use it with caution and not mess up the original routing state, so we need to restore the original state after completing the cloning. To prevent any accidents, use history.replaceState() instead of history.pushState().
const structuralClone = obj => {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
};
The advantage of this method is that it can solve the problem of circular objects, supports cloning of many built-in types, and is synchronous. The disadvantage is that some browsers have limitations on the frequency of calls. For example, Safari only allows 100 calls within 30 seconds.
Notification#
Notification is used for desktop notifications. It can also be used to deep copy JavaScript objects.
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
The advantage of this method is that it can solve the problem of circular objects, supports cloning of many built-in types, and is synchronous. The disadvantage is that using this API requires requesting permission from the user, but when cloning data, it can be used without user authorization. It will prompt you when used in an HTTP scenario instead of an HTTPS scenario.
For some reason, Safari always returns undefined.
lodash's deep copy function#
lodash's _.cloneDeep() supports circular objects and a large number of built-in types. It handles many details quite well and is recommended for use.
Solving Circular Reference#
By analyzing the above methods, it is found that only the structured clone algorithm can solve the problem of circular reference. So how can we solve it using native JavaScript? Here, we try to use closures to solve the circular reference problem.
function deepClone(object) {
const memo = {};
function clone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
if (memo[obj[key]]) {
result[key] = memo[obj[key]];
} else {
memo[obj[key]] = obj[key];
result[key] = clone(obj[key]);
}
}
return result;
}
return clone(object);
}
var obj = {};
var b = { obj };
obj.b = b;
var obj2 = deepClone(obj);
In the above code, the recursive copy has been optimized. Closures are used to solve the circular copy problem. The memo variable is used to record the copied reference address. Before each recursion, the parameter value of the recursion is recorded, so that the recursion can be exited within the recursion to solve the circular reference problem.
Summary#
- If you don't have circular objects and don't need to preserve built-in types, you can use the cross-browser JSON.parse(JSON.stringify()) to achieve the fastest cloning performance.
- If you want a proper structured clone, MessageChannel is your only reliable cross-browser choice.