Shallow and deep copies
Foreword
There are different ways to copy objects in javascript, if you are not familiar with the language, it is easy to fall into the trap when copying objects, so how can we copy an object properly?
After reading this article, I hope you can understand:
- What are deep/shallow copies and how are they different from assignment?
- How many ways are there to implement deep/shallow copy?
Shallow copy and deep copy
- A shallow copy creates a new object with an exact copy of the original object's property values. If the property is a basic type, the value of the basic type is copied, and if the property is a reference type, the memory address is copied, so if one object changes this address, it will affect the other object .
- Deep copy is to copy an object completely from the memory, open up a new area from the heap memory to store the new object, and modify the new object will not affect the original object .
var a1 = {b: {c: {}};
var a2 = shallowClone(a1); // shallow copy method
a2.b.c === a1.b.c // true old and new objects still share the same memory
var a3 = deepClone(a3); // deep copy method
a3.b.c === a1.b.c // false The new object does not share memory with the original object
With the help of the following two pictures of ConardLi , help us better understand the meaning of the two:
All in all, a shallow copy only copies a pointer to an object, not the object itself, and the old and new objects still share the same piece of memory . But deep copying will create another identical object, the new object does not share memory with the original object , and modifying the new object will not change the original object.
Difference between assignment and deep/shallow copy
The difference between the three is as follows, but the premise of the comparison is for reference types :
- When we assign an object to a new variable, it is actually the address of the object on the stack, not the data in the heap . That is, the two objects point to the same storage space. No matter which object changes, it is actually the content of the changed storage space. Therefore, the two objects are linked.
- Shallow copy: Re-create memory in the heap. The basic data types of objects before and after copying do not affect each other, but the reference types of objects before and after copying will affect each other because they share the same memory.
- Deep copy: open up a new area in the heap memory to store the new object, recursively copy the sub-objects in the object, and the two objects before and after the copy do not affect each other.
Let's first look at the following example, comparing the effect of assignment and deep/shallow copy on the original object after modification:
// object assignment
let obj1 = {
name : 'boating in the waves',
arr : [1,[2,3],4],
};
let obj2 = obj1;
obj2.name = "Alang";
obj2.arr[1] =[5,6,7];
console.log('obj1',obj1) // obj1 { name: 'Alang', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj2',obj2) // obj2 { name: 'Alang', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// shallow copy
let obj1 = {
name : 'boating in the waves',
arr : [1,[2,3],4],
};
let obj3=shallowClone(obj1)
obj3.name = "Alang";
obj3.arr[1] = [5,6,7] ; // The old and new objects still share the same memory
// This is a shallow copy method
function shallowClone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
console.log('obj1',obj1) // obj1 { name: 'boat in waves', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'Alang', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// deep copy
let obj1 = {
name : 'boating in the waves',
arr : [1,[2,3],4],
};
let obj4=deepClone(obj1)
obj4.name = "Alang";
obj4.arr[1] = [5,6,7] ; // The new object does not share memory with the original object
// This is a deep copy method
function deepClone(obj) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// implement a recursive copy
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
console.log('obj1',obj1) // obj1 { name: 'boat in the waves', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'Alang', arr: [ 1, [ 5, 6, 7 ], 4 ] }
In the above example, obj1 is the original object, obj2 is the object obtained by the assignment operation, the object obtained by the shallow copy of obj3, and the object obtained by the deep copy of obj4, through the following table, we can clearly see their impact on the original data:
Shallow copy implementation
1.Object.assign()
The Object.assign() method can copy any number of enumerable properties of the source object itself to the target object, and then return the target object.
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2. The _.clone method of the function library lodash
The library also provides _.clone for Shallow Copy, and we will introduce the use of this library to implement deep copy later.
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3. Spread operator...
The spread operator is an es6/es2015 feature that provides a very convenient way to perform a shallow copy, which does the same thing as Object.assign().
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4. Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
5. Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
Implementation of deep copy
1. JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
This is also to use JSON.stringify to convert the object into a JSON string, and then use JSON.parse to parse the string into an object. One by one, a new object is generated, and the object will open up a new stack to achieve deep copying.
Although this method can realize deep copying of arrays or objects, it cannot handle functions and regularities , because after these two are processed based on JSON.stringify and JSON.parse, the obtained regularity is no longer regular (it becomes an empty object), and the result is is no longer a function (becomes null).
For example the following example:
let arr = [1, 3, {
username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
2. The _.cloneDeep method of the function library lodash
The library also provides _.cloneDeep for Deep Copy
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
3. jQuery.extend() method
jQuery provides a $.extendtool that can be used to do Deep Copy
$.extend(deepCopy, target, object1, [objectN])//The first parameter is true, which is a deep copy
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
4. Handwritten recursive method
The recursive method implements the principle of deep cloning: traverse objects and arrays until they are all basic data types, and then copy them, which is a deep copy .
There is a special case to note that the object has a circular reference , that is, the property of the object directly refers to itself, to solve the circular reference problem, we can open up an additional storage space to store the current object and the corresponding relationship between the copied object, When you need to copy the current object, first go to the storage space to find out if the object has been copied, if so, return it directly, if not, continue to copy, so as to solve the problem of circular reference ingeniously.
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // if it is null or undefined I will not perform the copy operation
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// May be an object or a normal value If it is a function, no deep copy is required
if (typeof obj !== "object") return obj;
// If it is an object, a deep copy is required
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// The constructor on the prototype of the class to which it belongs is found, and the constructor on the prototype points to the current class itself
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// implement a recursive copy
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // the object has a circular reference
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);