Deep copy shallow copy and assignment principle and implementation analysis
We often use deep copy and shallow copy in our work, but have you analyzed what scenarios to use it, why to use it, what are the differences and similarities of deep copy, what is deep copy, how to implement it, do you have these questions, today for everyone summary.
Stack memory vs. heap memory
Before we can understand this, we need to understand the concept of stack and heap memory, which we can refer to in my previous article. It’s pretty easy to understand this once you know the concept.
The difference between
- Shallow copy – copies a pointer to an object, not the object itself. The copied objects share a pointer. One of them changes value, and the others change at the same time.
- Deep copy – copy out a new object, open up a new space, copy before and after the object is independent of each other, do not change each other, have different Pointers.
Under simple summary, it is assumed that there is A A, we copy A to B, is A time when modifying A or B to see another will also change, if you change the value of B also changed it is shallow copy, if change after the B value has not changed is deep copy, of course, this is the foundation to understand, let us together to analyze the below.
The assignment
/** demo1 Basic data type */
let a = 1;
let b = a;
b = 10;
console.log(a,b)// 1 10
/** demo2 references the data type */
let a = {
name: 'small nine'.age: 23.favorite: ['eat'.'sleep'.'Beat the beans']}let b = a;
a.name = 'seven'
a.age = 18
a.favorite = ['work'.'off'.'overtime']
console.log(a,b)
/ * * {name: "little seven, the age: 18, favorite: [' work 'and' work ', 'overtime']} {name: 'small seven, the age: 18, favorite: [' work' and 'work', 'overtime']} * /
Copy the code
, look at the example it can be seen through the assignment to get a new value, assignment for the basic data is on the stack has opened a new one variable, the equivalent of two independent stack memory, so will not affect each other, but for the reference data types, he just copy a copy of a memory of the stack pointer, so two Pointers point to the same heap memory space, By any change A pointer value will affect the other, through such assignment can produce multiple pointer, but the heap memory space is always only one, that is the problem of assignment, we certainly don’t want to change in the development and affected the A, B so shallow and deep copy them is required at this time.
- For primitive data types, arbitrary assignments do not affect each other
- For reference data types, assignments can do things we don’t want to see happen, with both sides changing.
Shallow copy
Object.assign()
/** Object.assign */
let A = {
name: 'small nine'.age: 23.sex: 'male'
}
let B = Object.assign( {}, A);
B.name = 'seven'
B.sex = 'woman'
B.age = 18
console.log(A,B)
/ * * {name: 'small nine, age: 23, sex:' male '} {name: 'small seven, the age: 18, sex:' female '} * /
Copy the code
The first way to implement shallow copy in the first place is through the object.assign () method, which is used to copy the values of all enumerable properties from one or more source objects to target objects. It will return the target object.
Anyway, let’s take A look at the results. We find that the value of A does not change after copying the name, age and sex of B. In this case, you might think, this is A success, AB and IKEA do not affect each other. But shallow copy will be AB and we described above is not change each other deep copy produced a contradiction, that is why, in fact had said to the above, this demo used are all basic data types, so copy and assignment, in view of the basic data types, are in the stack to create a variable, so it will not affect each other, So let’s look at reference data types
let A = {
name: 'small nine'.age: 23.sex: 'male'.favorite: {
item_a: ['Play games'.'the Internet'].item_b: ['read'.'online classes']}}let B = Object.assign( {}, A);
B.name = 'seven'
B.sex = 'woman'
B.age = 18
B.favorite.item_a =['Play basketball']
B.favorite.item_b =['Write notes']
console.log(A)
console.log(B)
/** Print the result comparison */
{ name: 'small nine'.age: 23.sex: 'male'.favorite: { item_a: [ 'Play basketball'].item_b: [ 'Write notes']}} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- {name: 'seven'.age: 18.sex: 'woman'.favorite: { item_a: [ 'Play basketball'].item_b: [ 'Write notes']}}Copy the code
Through comparison, we found that we also copied A and found that changing B’s name, age and sex did not affect A, but changing Facorite affected A, so the problem is that we still cannot meet our needs through shallow copy, and changing B also affected A. Back to this method, Object.assign() copies the enumerable attributes of any source Object to the target Object, and then returns the target Object. This method makes a shallow copy of the Object, copying references to the Object instead of the Object itself. We call object.assign () a shallow copy. This is only used when the Object has only one layer structure.
- A lot of people say
Object.assign
It’s a deep copy, which is actually wrong, - A shallow copy is a bitwise copy of an object that creates a new object with an exact copy of the original object’s property values. If the property is of a primitive type, the value of the primitive type is copied. If the property is a memory address (reference type), the memory address is copied, so if one object changes the address, the other object will be affected. That is, the default copy constructor only copies objects shallowly (member by member), that is, only the object space is copied, not the resource.
- This method copies only the properties of the source object itself, not its inherited properties.
- This method does not copy the object’s non-enumerable properties
- Undefined and NULL cannot be converted to objects. They cannot be used as object. assign arguments, but can be used as source objects
- The value with the attribute Symbol can be copied by this method.
- Shallow copy, which copies the basic data type structure of the first layer, but does not copy the underlying data type structure of the first layer
Array.prototype.silce
Before we look at this method let me show you the description of this method in MDN.
The return value
Returns a new array containing elements from the arrayObject from start to end (excluding the element).
instructions
Note that this method does not modify the array, but returns a subarray. If you want to delete an element from an Array, use the array.splice () method.
Now that you’ve seen the description of it, it should be pretty clear, but let’s go ahead and implement it using the example we just did
let A = [1.2.3[4.5]]
let B = A.slice();
B[3] = 4
console.log(A)
console.log(B)
/** 对比 */
[ 1.2.3[4.5]] [1.2.3.4 ]
Copy the code
Can be found that will not affect each other, and it has realized the shallow copy, in the same way for complex multi-layer data structure as before will influence each other, so people understand, the shallow word is also from this and come, so the above statement is not very ready, not necessarily AB is not affected, it must be a deep copy each other, also combined with the data structure level view.
Array.from()
Let’s start with a description of MDN
The array.from () method creates a new, shallow-copy Array instance from an array-like or iterable.
Array.form() is used to convert two types of objects into real arrays, one is like-array and the other is iterable, and we can use this method to make a shallow copy.
let A = [1.2.3[4.5]].let B = Array.from(A) ;
B[3] = 6
console.log(A)
console.log(B)
/** ** */
[ 1.2.3[4.5]] [1.2.3.6 ]
Copy the code
It can be found that the same effect can be achieved.
Array.prototype.concat
let A = [1.2.3[4.5]].let B = [].concat(A) ;
B[3] = 6
console.log(A)
console.log(B)
/** ** */
[ 1.2.3[4.5]] [1.2.3.6 ]
Copy the code
Array method principle is similar, the appropriate understanding of the line, you can operate to try.
ES6 -> []
ES6’s extended operators are also easy to do and very handy to look at
let A = [1.2.3[4.5]]
let B =[...A]
B[3] = 6
console.log(A)
console.log(B)
/** ** */
[ 1.2.3[4.5]] [1.2.3.6 ]
Copy the code
The extension operator is a new feature in ES6, it is very powerful and very convenient, and it is also a way I love to use everyday. Objects and arrays can be operated, in addition to the easy implementation of shallow copy, merge objects is also very easy, you can use more.
for in
I’m going to write a simple version, because this is also a deep copy, so I’m going to do it,
let A = [1.2.3[4.5]]
let B = []
for (var i in A){
B[i] = A[i]
}
B[3] = 9
console.log(A,B)
/** ** */
[ 1.2.3[4.5]] [1.2.3.9 ]
Copy the code
Found that the same can be achieved, the principle is also very simple, self-analysis.
There are many ways to implement shallow copies, not just the six I have listed here, but of course, in real development, we focus more on deep copies, so let’s look at how to implement a deep copy.
Deep copy
JSON.parse(JSon.stringify())
Parse (json.stringify ()) */
let A = {
a: 1.b: 2.c: [4.5.6]}let B = JSON.parse(JSON.stringify(A))
B.a = 2
B.b = 3
B.c = 4
console.log(A == B)
console.log(A,B)
Comparison / * * * {2, a: 1, b: c: [4, 5, 6]} * {2, a: b: 3, c: 4} * /
Copy the code
Json.stringify (), json.parse (), json.parse () Json is first serialized (json string) through Stringify, and then unserialized (parse) JS objects. Serialization is used for storage and transfer. During this process, new memory space is opened, which creates space different from the source object and enables deep copy. In practice, this usage has been used to solve many scenarios, but there are still many drawbacks.
- If obj has a time object in it, json.stringify will be followed by json.parse, and the time will be just a string. Rather than a time object;
- If there are RegExp and Error objects in obj, the serialized result will be empty objects.
- If obj has a function called undefined, the serialization result will lose the function or undefined.
- If obj has NaN, Infinity, and -infinity, the serialized result becomes null
- Json.stringify () can serialize only the enumerable properties of an object, for example, if the object in obj is generated by a constructor, the constructor of the object is discarded when this method is used.
- This method cannot copy the function type
In summary, this method has a number of problems, of course, for a qualified programmer, this version is too low, we certainly want to implement more comprehensive.
Base version (shallow copy)
/** Base version for in */
let A = {
a: [1.2.3].b: { a: 1.b: 2},
c: 99
}
function deepClone(target) {
let return_result = {}
for(let key in target) {
return_result[key] = target[key]
}
return return_result
}
let B = deepClone(A)
B.a= 99
B.b = 88
console.log(A,'-- -- -- -- -- -- -- -- -- --',B)
Comparison / * * * {a: [1, 2, 3], b:, 2} {a: 1, b: c: 99} * {a: 99, b: 88 c: 99} * /
Copy the code
As you can see, we can implement a basic shallow copy with for in, just like object.assign (), which can only copy the first layer, but we have a preliminary success.
The next thing we need to consider is that we need to consider the array, it can only be objects, also very simple, we just need to add a judgment, next change:
Compatible array (shallow copy)
/** Base version for in + compatible array */
let A = [1.2.3[4.5]]
function deepClone(target) {
if (typeof target == 'object') {// Check whether the data type is referenced first
let return_result = Array.isArray(target) ? [] : {}
for(let key in target) {
return_result[key] = target[key]
}
return return_result
}else{
returntarget; }}let B = deepClone(A)
B[3] =99
B[2] = 88
console.log(A,'-- -- -- -- -- -- -- -- -- --',B)
Comparison / * * * [1, 2, 3, [4, 5]] * [1, 2, 88, 99] * /
Copy the code
As you can see, we are now compatible with arrays, but still not enough. We can only copy the first layer, so we need to recursively copy the objects on the deep side.
Base version + Compatible array + recursive call (deep copy)
/** compatible array + recursive call */
function deepClone(target) {
let result;
if (typeof target === 'object') {
if (Array.isArray(target)) {
result = []
for (let i in target) {
result.push(deepClone(target[i]))
}
} else {
result = {}
for (let key in target) {
result[key] = target[key]
}
}
} else {
result = target;
}
return result;
}
let A = [1.2.3, { a: 1.b: 2 }]
let B = deepClone(A)
B[3].a = 99
console.log(A)
console.log(B)
Comparison / * * * [1, 2, 3, [4, 5]] * [1, 2, 3, 2} {a: 99, b:] * /
Copy the code
We first determine the type, to objects and arrays are recursive calls, if is the basic data type is direct assignment, so far, we have been able to complete a foundation of deep copy, but is not enough, because we only do to array type judgment here, other default are object, but the actual situation will also have many types, for example, the RegExp, Date, Null, Undefined, function, etc., so we will improve it, add all the judgment, because there are many types, we can separate the judgment of the object, and improve it together: Another point we need to consider before we do this is the cyclic reference problem with JS. The current method is problematic when copying objects with cyclic references.
/** Base version for in + compatible array + recursive call + circular reference problem */
function deepClone(target) {
let result;
if (typeof target === 'object') {
if (Array.isArray(target)) {
result = []
for (let i in target) {
result.push(deepClone(target[i]))
}
} else {
result = {}
for (let key in target) {
result[key] = target[key]
}
}
} else {
result = target;
}
return result;
}
let A = [1.2.3, { a: 1.b: 2 }]
A[4] = A
let B = deepClone(A)
console.log(A,B)
/** RangeError: Maximum call stack size exceeded */
Copy the code
There will be an error that exceeds the maximum call stack size, which is also a pit in the deep copy. Here we can solve this problem by using a type of WEAKMap of JS, which can be learned by reading the MDN documentation:
The native WeakMap holds a “weak reference” to each key object, which means garbage collection works correctly when no other reference exists. The structure of the native WeakMap is special and efficient, and the key it uses for mapping is valid only if it is not reclaimed.
Because of such weak references, WeakMap keys are not enumerable (there is no way to give all keys). If keys were enumerable, their lists would be subject to garbage collection, resulting in inconclusive results. Therefore, if you want a list of keys for objects of this type, you should use Map.
Basically, if you want to add data to an object and don’t want to interfere with garbage collection, you can use WeakMap.
Solution: Use a WeakMap structure to store the object that has been copied. When copying each time, first check whether the object has been copied to WeakMap. If it has been copied, take out the object and return.
/** Base version for in + compatible array + recursive call + solve circular reference problem */
function deepClone(val, hash = new WeakMap(a)) {
if (hash.has(val)) return hash.get()
let cloneVal;
if (isObj(val)) { // Check if it is a reference type
if (Array.isArray(val)) { // Check whether it is an array
cloneVal = []
hash.set(val, cloneVal)
for (let i in val) {
cloneVal.push(deepClone(val[i]))
}
}
else {
cloneVal = {}
hash.set(val, cloneVal)
for (let key in val) {
cloneVal[key] = val[key]
}
}
} else {
cloneVal = val;
}
return cloneVal;
}
/** Is a reference type */
function isObj(val) {
return (typeof val == 'object' || typeof val == 'function') && val ! =null
}
var a = {}
a.a = a
var b = deepClone(a)
console.log(b)
Copy the code
In this way, we can initially solve the problem of circular calls. The next thing to consider is how to do different processing for more types. We borrow a previous article on detecting JS types, and use this method to detect data types through JS to handle various types separately.
/** Full version */
function deepClonea(val, map = new WeakMap(a)) {
let type = getType(val); // Get the specified type first when a reference type is used
if (isObj(val)) {
switch (type) {
case 'date': // The date type is passed the previous value again, the date instantiation itself remains the same
return new Date(val);
break;
case 'regexp': // The regex type is simply new and passed into source and flags
return new RegExp(val.source, val.flags);
break;
case 'function': // If it is a function type, return a new function directly through the function wrapper and change the this reference
return new RegExp(val.source, val.flags);
break;
default:
let cloneVal = Array.isArray(val) ? [] : {};
if (map.has(val)) return map.get(val)
map.set(val, cloneVal)
for (let key in val) {
if (val.hasOwnProperty(key)) { // Determine if it is its own key
cloneVal[key] = deepClone(val[key]), map;// Each of the basic types needs to be copied using the DeepClone method}}returncloneVal; }}else {
return val; // Return it directly if it is a basic data type}}function isObj(val) { // Check if it is a reference type
return (typeof val == 'object' || typeof val == 'function') && val ! =null
}
function getType(data) { // Get the type
var s = Object.prototype.toString.call(data);
return s.match(/\[object (.*?)\]/) [1].toLowerCase();
};
// ** test */
var a = {}
a.a = a
var b = deepClonea(a)
console.log(b)
Copy the code
Final full version
Now that we have a deep copy that can handle most scenarios, let’s use the class method to modify it so that we can extend it later.
/** write */ instead of class
class DeepClone {
constructor(){
cloneVal: null;
}
clone(val, map = new WeakMap(a)) {
let type = this.getType(val); // Get the specified type first when a reference type is used
if (this.isObj(val)) {
switch (type) {
case 'date': // The date type is passed the previous value again, the date instantiation itself remains the same
return new Date(val);
break;
case 'regexp': // The regex type is simply new and passed into source and flags
return new RegExp(val.source, val.flags);
break;
case 'function': // If it is a function type, return a new function directly through the function wrapper and change the this reference
return new RegExp(val.source, val.flags);
break;
default:
this.cloneVal = Array.isArray(val) ? [] : {};
if (map.has(val)) return map.get(val)
map.set(val, this.cloneVal)
for (let key in val) {
if (val.hasOwnProperty(key)) { // Determine if it is its own key
this.cloneVal[key] = newDeepClone().clone(val[key], map); }}return this.cloneVal; }}else {
return val; // Return it directly if it is a basic data type}}/** Check if it is a reference type */
isObj(val) {
return (typeof val == 'object' || typeof val == 'function') && val ! =null
}
/** Get the type */
getType(data) {
var s = Object.prototype.toString.call(data);
return s.match(/\[object (.*?)\]/) [1].toLowerCase();
};
}
/ * * * / test
var a ={
a:1.b:true.c:undefined.d:null.e:function(a,b){
return a + b
},
f: /\W+/gi,
time: new Date(),}const deepClone = new DeepClone()
let b = deepClone.clone(a)
console.log(b)
Copy the code
Well, the above is a deep copy of this summary, of course, it is not perfect, there are many more scenarios, may be added later, but this is enough for a large part of your scenario.