在JavaScript开发中,理解浅拷贝与深拷贝的区别是非常重要的,尤其是当我们处理引用数据类型(如对象、数组和函数)时。下面,我们将讲解浅拷贝和深拷贝的概念、它们的工作原理和实现方法,并介绍WeakSet和WeakMap的概念。最后,我们将探讨如Lodash这样的第三方库是如何实现深拷贝的。
浅拷贝
这个过程只涉及到对象最表面一层的复制。这就意味着,浅拷贝创建的新对象,在其内部结构方面,始终与原对象保持着一种"隐形的纽带"。即使是新对象,其一些深层次的属性依然紧紧地绑定着原对象,形成一种不可见的联系。因此,每当原始对象中的这些共享属性发生改变时,浅拷贝出的新对象中相应的属性也会实时地受到影响。
浅拷贝的实现可以通过以下方法:
对象的浅拷贝
-
使用
Object.assign()
方法:iniconst originalObj = { a: 1, b: { c: 2 } }; const shallowCopyObj = Object.assign({}, originalObj);
这里,
shallowCopyObj
是originalObj
的一个浅拷贝。如果修改shallowCopyObj.b.c
的值,originalObj.b.c
的值也会改变,因为内部对象是通过引用拷贝的。 -
使用对象扩展运算符(
...
):iniconst originalObj = { a: 1, b: { c: 2 } }; const shallowCopyObj = { ...originalObj };
和
Object.assign()
类似,这也是一个浅拷贝。内部对象的更改会影响到原始对象。 -
使用
Object.create()
方法:iniconst originalObj = { a: 1 }; const shallowCopyObj = Object.create(Object.getPrototypeOf(originalObj), Object.getOwnPropertyDescriptors(originalObj));
注意:
Object.create()
更常用于设置原型链而不是拷贝对象,但可以用上述方式实现浅拷贝。通常,它不直接用于浅拷贝,因为它创建的是一个新对象,其原型指向传入的对象。
数组的浅拷贝
-
使用
concat()
方法:iniconst originalArray = [1, 2, { a: 3 }]; const shallowCopyArray = originalArray.concat();
这里,
shallowCopyArray
是originalArray
的一个浅拷贝。如果修改shallowCopyArray[2].a
的值,originalArray[2].a
的值也会改变。 -
使用
slice()
方法:iniconst originalArray = [1, 2, { a: 3 }]; const shallowCopyArray = originalArray.slice();
同样,这也是一个浅拷贝。数组内部对象的更改会影响到原始数组。
-
使用数组解构:
iniconst originalArray = [1, 2, { a: 3 }]; const shallowCopyArray = [...originalArray];
这也创建了一个浅拷贝。就像前面的方法一样,对内部对象的修改会反映在原始数组中。
深拷贝
相对于浅拷贝,深拷贝递归拷贝所有层次的属性。这意味着它不仅复制对象的直接属性,还复制每个属性指向的对象,一直递归下去。因此,原始对象和深拷贝对象结构完全相同,但没有任何共享的部分。深拷贝开辟一个新的栈,两个对象完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
使用 JSON.parse(JSON.stringify(obj)) 实现深拷贝
假设我们有一个JavaScript对象,该对象包含各种嵌套的属性,包括数组、嵌套对象和基本数据类型:
css
const original = {
name: "John",
age: 30,
details: {
hobby: "reading",
address: {
city: "New York",
country: "USA"
}
},
tags: ["developer", "reader"]
};
要使用 JSON.parse(JSON.stringify(obj))
对此对象进行深拷贝,我们首先将对象转换为JSON字符串,然后将该字符串解析回JavaScript对象:
ini
const deepCopied = JSON.parse(JSON.stringify(original));
现在,deepCopied
是 original
的一个深拷贝,这意味着我们可以修改 deepCopied
中的属性而不影响 original
对象:
ini
deepCopied.details.address.city = "San Francisco";
console.log(original.details.address.city); // 输出 "New York"
在上面的例子中,修改 deepCopied
对象的 details.address.city
属性不会影响 original
对象的同一属性,证明了深拷贝已经成功。
局限性
虽然 JSON.parse(JSON.stringify(obj))
是一种便捷的深拷贝实现方式,它也有一些局限性:
- 无法复制函数:如果原始对象中包含函数,这些函数不会被复制到新对象中。
- 无法处理循环引用:如果原始对象中存在循环引用,这种方法会抛出错误。
- 特殊对象问题 :日期对象(
Date
)、正则表达式(RegExp
)、Map
、Set
等特殊对象类型在深拷贝后可能不会保留其原始结构或类型。 - 忽略undefined和symbol属性 :如果对象中包含
undefined
或symbol
类型的属性,在深拷贝过程中这些属性会被忽略。
使用递归手写
scss
function deepCopy(obj){
if(obj === null || typeof obj !== 'object') return obj;
let objCopy = Array.isArray(obj) ? [] : {};
for(let key in obj){
if(obj.hasOwnProperty(key)){
objCopy[key] = deepCopy(obj[key]);
}
}
return objCopy;
}
这个 deepCopy
函数会检查每个属性是否为引用类型,并递归调用自己;如果不是,它会直接复制值。
Lodash 的 _.cloneDeep
Lodash 提供的 _.cloneDeep
方法实现了深拷贝,能够处理各种类型的值,包括数组、对象、Map、Set等,并且它也能处理循环引用的情况。
使用 _.cloneDeep
的示例:
ini
const _ = require('lodash');
const object = { 'a': 1, 'b': { 'c': 2 } };
const deepCopy = _.cloneDeep(object);
WeakSet 和 WeakMap
WeakSet
和 WeakMap
是ES6中引入的两种新的数据结构,主要区别于 Set
和 Map
在于它们只持有对象的弱引用,这意味着它们不阻止其引用的对象被垃圾回收。
WeakSet
是一个不允许重复的值的集合,但这些值必须是对象。WeakMap
是键值对的集合,但键必须是对象,值可以是任意数据类型。
这些特性使它们成为管理对象私有数据或缓存数据的好工具,而不必担心内存泄漏问题。
处理循环引用
在实现深拷贝时,处理循环引用是非常重要的,以避免无限递归导致的程序崩溃。我们可以创建一个 WeakMap
来跟踪已经被复制过的对象,如下示例所示:
ini
function deepCopy(obj, weakMap = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (weakMap.has(obj)) {
return weakMap.get(obj);
}
let clone = Array.isArray(obj) ? [] : {};
weakMap.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepCopy(obj[key], weakMap);
}
}
return clone;
}