一、浅拷贝
1、定义:
浅拷贝是指复制一个对象的引用,并不是复制对象本身。在JavaScript中,我们可以使用Object.assign()方法实现对象的浅拷贝。
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 0 } }
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 3 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 3 } }
由上面的代码可以看出,使用Object.assign()方法可以实现对象的浅拷贝,当只是拷贝了第一层的数据时,obj1和obj2并不会互相影响。但是,当拷贝了第二层及其以上层次时,obj1和obj2就会产生影响,因为对象的属性b被拷贝的时候,并没有复制其引用指向的内存空间,而是复制了其引用值,这就导致obj1和obj2的属性b所指向的内存空间是相同的。因此,当我们修改属性b的值时,两个变量会产生相同的变化,这就是典型的浅拷贝问题。
2、浅拷贝的应用场景:
浅拷贝适用于不包含引用类型数据的对象或者数组。
二、深拷贝
1、定义:
深拷贝是指对一个对象的完全拷贝,即便这个对象的属性值也是引用类型,也能够递归地拷贝子对象的所有属性。在JavaScript中,我们可以使用JSON.parse(JSON.stringify(obj))方式实现对象的深拷贝。
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 0 } }
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 3 } }
使用JSON.parse(JSON.stringify(obj))方式可以实现对象的深拷贝,即使对象的属性值也是引用类型,也能够递归地拷贝子对象的所有属性。由此可见,深拷贝可以真正意义上地将两个对象相互独立开来,互不影响。
2、深拷贝的应用场景:
深拷贝适用于包含引用类型数据的对象或者数组。
三、深浅拷贝的实现方式
1、基于JSON.stringify()和JSON.parse()的深拷贝实现:
function deepClone_Json(obj) {
let str = JSON.stringify(obj);
return JSON.parse(str);
}
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = deepClone_Json(obj1);
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 0 } }
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 3 } }
通过重写JSON.stringify()和JSON.parse()方法可以实现深拷贝,但是这种方法存在以下几个问题:
- 该方法无法拷贝函数,RegExp对象等特殊对象。
- 在部分特殊场景下,该方法可能会抛出异常。
- 该方法无法解决循环引用的问题。
2、递归方式的深拷贝实现:
function deepClone_Recursion(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone_Recursion(obj[key]);
}
}
return newObj;
}
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = deepClone_Recursion(obj1);
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0 } }
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 0 } }
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 } }
console.log(JSON.stringify(obj2)); // { "a": 2, "b": { "c": 3 } }
通过递归的方式实现深拷贝,可以解决前面所提到的问题,但是这种方法在处理循环引用的时候,难免会陷入死循环,导致浏览器崩溃。
3、利用WeakMap解决循环引用的深拷贝实现:
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
let newObj = obj instanceof Array ? [] : {};
map.set(obj, newObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key], map);
}
}
return newObj;
}
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = { a: obj1, b: obj1 };
obj1.c = obj2;
let obj3 = deepClone(obj1);
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { "a": 1, "b": { "c": 0 }, "c": { "a": {...}, "b": {...} } }
console.log(JSON.stringify(obj3)); // { "a": 0, "b": { "c": 0 }, "c": { "a": {...}, "b": {...} } }
利用WeakMap来解决循环引用问题的深拷贝实现方式是目前最优的解决方案之一。WeakMap是ES6新增的一种Map类型,与Map类型相比,WeakMap只能用对象作为键,当对象被垃圾回收机制回收时,相应的键值对也会从WeakMap中删除,从而避免内存泄漏。通过在递归过程中维护一个map,我们可以记录已经拷贝过的对象,避免循环引用陷入死循环。
四、总结
深拷贝与浅拷贝是JavaScript编程中不可避免的问题,尤其是在处理对象和数组时尤为突出。深浅拷贝的实现方式也有多种,每种方式都有其优缺点。在实际开发中,我们需要根据具体场景来选择最适合的拷贝方式,避免因为深浅拷贝问题导致程序出现不可预期的错误。