深拷贝和浅拷贝的区别,以及对于循环引用如何处理深拷贝
浅拷贝仅拷贝对象的第一层属性值,对于基本数据类型,会复制其值;对于引用数据类型,仅复制引用地址而不复制实际的对象内容。浅拷贝后的新对象与原对象中的引用类型属性仍指向同一个内存地址。
const obj1 = {name: "Alice",details: { age: 25 }
};
const obj2 = { ...obj1 }; // 浅拷贝
obj2.name = "Bob";
obj2.details.age = 30;
console.log(obj1.name); // "Alice",浅拷贝不会影响原始对象的基本类型属性
console.log(obj1.details.age); // 30,但嵌套对象被引用,修改后会影响原对象
在这个例子中,obj2.details
和 obj1.details
指向同一个对象,因此修改 obj2.details.age
会影响 obj1.details.age
。
``
实现浅拷贝的常见方法
Object.assign()
- 扩展运算符
{ ...obj }
Array.prototype.slice()
和Array.prototype.concat()
(用于数组)
2. 深拷贝
深拷贝会递归地拷贝对象的每一层,生成一个与原始对象内容完全独立的新对象。深拷贝后的对象和原对象没有任何引用关系,对新对象的任何更改都不会影响原对象。
代码示例
const obj1 = {
name: "Alice",
details: { age: 25 }
};const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝obj2.name = "Bob";
obj2.details.age = 30;console.log(obj1.name); // "Alice",深拷贝不会影响原始对象
console.log(obj1.details.age); // 25,深拷贝后,嵌套对象也被完全独立
在这个例子中,obj2
是 obj1
的深拷贝,obj2.details
和 obj1.details
指向不同的内存地址,因此修改 obj2.details.age
不会影响 obj1.details.age
。
实现深拷贝的方法
JSON.parse(JSON.stringify(obj))
(有局限性,不适用于循环引用、特殊对象类型如函数、Date
、RegExp
、Map
、Set
、undefined
等)- 递归拷贝方法
- 使用第三方库(如
lodash.cloneDeep
)
以下是我自己实现的一个深copy 的方法(可以处理循环引用)
var obj = {a: 1,c: [1, 2, 3, {name: 'deep',type: {hello: ['hello1', 'hello2', 'hello3']}}]
};
obj.b = obj.a;
const deepCopy = () => {const hash = new WeakMap()return function copyInner(data) {if (typeof data !== 'object' || data === null) {return data}if (hash.has(data)) {return hash.get(data);}const copy = Array.isArray(data) ? [] : {};hash.set(data, copy);for (let key in data) {if (data.hasOwnProperty(key)) {copy[key] = copyInner(data[key], hash); // 递归拷贝}}return copy}
}
const datacopy = deepCopy();
const newdata = datacopy(obj)
newdata.c[2] = 'gogo';
newdata.c[3].type.hello[2] = 'gogo1'
console.log(newdata, obj);
以下是打印结果
在深拷贝中使用 const hash = new WeakMap()
是为了处理循环引用,同时提升深拷贝的性能和内存管理效率。
使用 WeakMap
的原因
-
处理循环引用
- 如果对象内部存在循环引用,普通的递归拷贝会导致无限循环。例如,
obj.b = obj
会使对象引用自身,直接递归调用会导致“超出调用堆栈”错误(Maximum call stack size exceeded
)。 - 通过
WeakMap
保存对象的引用关系,在递归过程中,如果发现某个对象已存在于WeakMap
中,则直接返回该对象的拷贝,避免重复拷贝和无限递归。
- 如果对象内部存在循环引用,普通的递归拷贝会导致无限循环。例如,
-
防止重复拷贝
- 对象可能在结构中多次出现,不使用
WeakMap
可能会导致冗余的深拷贝操作,浪费内存。 - 使用
WeakMap
可以在深拷贝时为每个对象生成一个唯一的拷贝引用,提升性能。
- 对象可能在结构中多次出现,不使用
-
自动垃圾回收
-
WeakMap
对键值的引用是“弱引用”,这意味着一旦键不再被其他引用,它可以被垃圾回收,从而自动释放内存。 -
使用
WeakMap
可以在完成深拷贝后确保那些不再使用的对象能够被自动回收,优化内存管理。
-
虽然 JSON.stringify
和 JSON.parse
来实现深拷贝的方式虽然简单高效,但它有一些显著的缺点和局限性,主要体现在以下几个方面:
1. 无法拷贝函数
JSON.stringify
会忽略对象中的函数属性。因此,如果对象包含函数,使用这种方法深拷贝时会丢失这些函数。
const obj = { name: "Alice", greet:function () { console.log("Hello!"); }
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.greet); // undefined
2. 无法拷贝特殊对象类型
JSON.stringify
只支持 JavaScript 的基本数据类型(如字符串、数字、数组、普通对象等),不支持一些特殊对象类型,如Date
、RegExp
、Map
、Set
、undefined
等。- 拷贝后的
Date
对象会变成字符串,而RegExp
对象会变成空对象。
const obj = { date: new Date(), regex: /test/i };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.date); // 转成了字符串
console.log(copy.regex); // {}
3. 无法处理循环引用
JSON.stringify
不能序列化具有循环引用的对象,直接使用会导致抛出TypeError
错误。
const obj = {};
obj.self = obj; // 循环引用
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
4. 拷贝数据可能不准确
JSON.stringify
对NaN
、Infinity
和-Infinity
的处理会将它们转换为null
。- 类似地,
undefined
属性会被忽略掉,这会导致拷贝的对象缺失原有的数据。
const obj = {value: NaN,infinity: Infinity,undefinedValue: undefined
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // { value: null, infinity: null }
5. 性能问题
JSON.stringify
在处理大对象时性能会有影响,因为它会将整个对象转换为字符串,这对处理复杂的嵌套结构会有一定的性能损耗。
总结
由于这些局限性,JSON.stringify
和 JSON.parse
的深拷贝方法只适合用于结构简单的对象和数组,不包含函数、循环引用和特殊对象。如果要深拷贝复杂结构的对象,推荐使用递归或其他第三方库(如 lodash.cloneDeep
)来实现。