— This article is taken from my official account “Sun Wukong, Don’t talk Nonsense”
Why have deep and shallow copies?
Before we answer that question, let’s take a look at data types in JavaScript.
Every value in the JavaScript language belongs to a certain data type. There are 7 data types specified in JavaScript language, namely: Undefined, Null, Boolean, String, Number, Symbol and Object.
These can be divided into two main categories: basic data types and reference data types.
Basic data types
- Definition: Basic data types refer to simple data segments. Basic types are accessed by value because you can manipulate the actual value stored in the variable.
- Storage: Values of basic types occupy a fixed amount of memory and are stored in stack memory. Copying a value of a primitive type from one variable to another creates a copy of that value.
- Usage: You cannot add attributes to values of primitive types.
Undefined, Null, Boolean, String, Number, and Symbol are basic data types.
Reference data type
- defineReference type values are objects that can be composed of multiple values.JavaScript does not allow direct access to locations in memoryThat is, the memory space of the operation object cannot be accessed directly. When you manipulate an object, you’re actually manipulating a reference to the object rather than the actual object.
- Storage: The value of a reference type is an object, which is stored in heap memory. The variable containing the value of a reference type actually contains not the object itself, but a pointer to the object. Copying a reference type value from one variable to another is actually copying a pointer, so both variables end up pointing to the same object.
- Usage: You can add attributes and methods to, change and delete attributes and methods for values of reference types.
In addition to the six basic data types mentioned above, all that remains are reference types, collectively known as Object types. Subdivided, there are: Object, Array, Date, RegExp, Function, and so on. A more detailed breakdown of types can be found here: Do you know all type objects?
The picture above says:
Why deep and light copies are needed
Because of this mechanism, when we copy the value of a reference type from one variable to another, we are actually copying the stack address of the reference type to the new variable, which is actually a pointer. So when the operation is done, these two variables actually refer to the same object in the heap, and changing either object will change the other object.
At some point, we don’t want to “change one object and the other will change,” so we have shallow copy.
One thing to be clear about here is that deep and shallow copies only occur in reference types.
The difference between deep and shallow copies
So what’s the difference between a deep copy and a shallow copy?
level
- Shallow copy: Only the properties of an object are copied sequentially, not recursively. That is, only the first-layer properties of the target object are assigned in the shallow copy.
- Deep copy: Copies not only the first layer properties of the target object, but all properties of the target object recursively.
Whether to open up new memory
- Shallow copy Directly assigns values to the data of the basic data type at the first layer of the target object, that is, “passes values”. However, for the data of reference data type in the first layer of the target object, that is, the heap memory address directly stored in the stack memory, that is, “address”, no new memory is opened. The result of copying is that both objects point to the same address. If you change the properties of one object, the properties of the other object will change.
- Deep copy and deep copy is to create a new memory, two objects correspond to two different addresses, modify the properties of one object, does not change the properties of the other object.
Shallow copy implementation
Common shallow copy implementations are as follows:
- For common objects: object.assign (). Note: This is a new syntax in ES6.
- For Array objects: array.prototype.concat (), array.prototype.slice ().
- Tip: ES6 expansion syntax (…) Can also be used for shallow copy oh.
Shallow copy implementation is relatively simple, the above mentioned several ways are not to expand the explanation. Here’s just one general way that isn’t limited to ES6 and array objects:
function shallowClone(source) {
var target = {};
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key]; }}return target;
}
Copy the code
HasOwnProperty and for… [[Prototype]];
for … The in loop iterates through all enumerable properties of its own object and its [[Prototype]] Prototype chain. HasOwnProperty, by contrast, only checks if the property is on the object, not the [[Prototype]] Prototype chain.
Deep copy implementation
As for the implementation of deep copy, let’s first look at the specific implementation method, and then talk about the necessity.
Before writing a logic implementation, it is a good practice to write test code to verify the feasibility of the logic. So here’s a common test case:
var testSample = {
name: "frank shaw",
work: {
job: "FEer",
location: "SZ"
},
a1: undefined,
a2: null,
a3: 123
}
Copy the code
Step 1: Simple implementation
The idea of a simple implementation is pretty straightforward: shallow copy + recursion. We can borrow the generic shallow copy of the code above, plus recursion, and have a simple implementation version:
/ / versionfunction deepClone1(source) {
var target = {};
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if(typeof source[key] == 'object'Target [key] = deepClone1(source[key]);
} else {
target[key] = source[key]; }}}return target;
}
Copy the code
Let’s look at the version 1 implementation, which is crude. There are the following obvious defects:
1, no validation is performed on the passed argument. Null should be returned instead of {};
Typeof null === ‘object’;
3. Array compatibility is not considered;
Let’s consider an improved version.
Step 2: Copy the array
First add a function that checks whether it is an object:
function isObject(obj){
return typeof obj === 'object'&& obj ! = null; }Copy the code
Because typeof NULL === ‘object’, an && judgment is added to the above code.
Array.prototype.isarray (); array.prototype.isarray ();
The code of the improved version is as follows:
/ / version 2function deepClone2(source) {// For non-object types, return directly, referring to basic and reference types aboveif(! isObject(source)) return source; Var target = array.isarray (source)? [] : {};for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if(isObject(sourceTarget [key] = deepClone2(source[key]);
} else {
target[key] = source[key]; }}}return target;
}
Copy the code
Step 3: Loop references
A circular reference chestnut can add a variable circleLink to our test case testSample to reference testSample itself. If this situation is not handled properly, it may lead to an infinite loop. How to solve it?
In fact, the general solution to circular references is to use a container to store previously used variables. To determine whether a circular reference is possible, ask the container if it contains a particular variable. If the container contains, use the variables of the container; If not, have the container record it.
Here, JavaScript built-in WeakMap is used as a container (of course, WeakMap is new in ES6, and arrays can be used as containers in ES5). The code is as follows:
/ / version 3function deepClone3(source, bucket = new WeakMap()) {
if(! isObject(source)) return source; // Check whether there are variables in the containersource
if(bucket.has(source)) return bucket.get(source);
var target = Array.isArray(source)? [] : {}; // Add bucket.set(source,target);
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if(isObject(source[key])){
//递归,将bucket容器传入
target[key] = deepClone3(source[key], bucket);
} else {
target[key] = source[key]; }}}return target;
}
Copy the code
Using the above method, you solve the problem of “lost references” as well as the problem of circular references. Great~
Step 4: Crack the recursive stack burst
When the recursion level is too deep, there will be the danger of stack overflow (i.e., stack burst), generally we consider to use a loop instead of recursion to solve the problem of stack overflow. So how do you do that?
We can think of a multi-layer nested data structure as a tree. Here’s an example:
var a = {
a1: 1,
a2: {
b1: 1,
b2: {
c1: 1
}
}
}
Copy the code
Think of it as a tree and it looks like this:
The structure of each node in the tree is:
letNode = {parent, // parent key, //key part data //value part}Copy the code
To loop through a tree, you need a stack. When the stack is empty, the traversal is complete, and the top store is the next node to be copied.
/ / version 4function deepClone4(source) {
if(! isObject(source)) return source; // container to solve the problem of circular applicationletbucket = new WeakMap(); // Return a new tree (new root node)let root = Array.isArray(source)? [] : {}; // Stack array initialization, put insourceElement const stack = [{parent: root, key: undefined, data:source,}];while(stack.length) {// breadth-first algorithm const node = stack.pop(); const parent = node.parent; const key = node.key; const data = node.data; // Initialize the assignment target. If key is undefined, copy it to the parent element, otherwise copy it to the child elementlet target = parent;
if(typeof key ! = ='undefined') { target = parent[key] = Array.isArray(data) ? [] : {}; } // Determine whether each Object appears in the bucketif(isObject(data)){
if(bucket.has(data)){
parent[key] = bucket.get(data);
continue; // break the loop} bucket.set(data, target); }for(let k in data) {
if (Object.prototype.hasOwnProperty.call(data, k)) {
if(isObject(data[k])) {// Next loop stack.push({parent: target, key: k, data: data[k],}); }else{ target[k] = data[k]; }}}}return root;
}
Copy the code
The code logic is not hard. A version of this simple implementation can be run.
conclusion
This is the discussion of light copy. In fact, the above implementation of the code, implementation is very basic functions (array, circular reference, recursive stack burst).
More considerations, such as Symbol type, Map type, Buffer type, prototype chain, and DOM element copy, are not taken into account. CloneDeep () and jquery.extend () from the well-known Lodash library on the web are excellent implementations of excellent depth copying. Those who are interested can go and have a look.
Finally, one small point to talk about is: Is deep copy necessary?
The necessity of deep copy
Parse (json.stringify (obj)) As we all know, JavaScript at the language level provides us with a deep-copy method that can be used directly. This serialization of a JSON string actually has a very limited scope.
So why don’t JavaScript officials provide us with a deep-copy method that we can use directly, instead we need to implement it ourselves?
The bottom line is that there are too many types of objects to consider if the official implementation of a deep-copy method can be arbitrarily used (for type objects, see here: Do you know all of them?). , performance is bound to be poor. This is a thankless task.
In fact, we don’t have to use deep copy for most of our daily development work. Also, using a deep-copy approach (even one with open source libraries on the web) can cause unexpected performance problems if you don’t know what it’s doing behind the scenes.
So, here’s a tip: don’t use deep copy if you can.
JavaScript in-depth series:
“var a=1;” What’s going on in JS?
Why does 24. ToString report an error?
Here’s everything you need to know about “JavaScript scope.
There are a lot of things you don’t know about “this” in JS
JavaScript is an object-oriented language. Who is for it and who is against it?
Deep and shallow copy in JavaScript
JavaScript and the Event Loop
From Iterator to Async/Await
Explore the detailed implementation of JavaScript Promises