当我们需要在进行对象深拷贝时,我们可能会遇到一个常见的问题:循环引用。这个问题指的是对象中存在对自身的引用,导致深拷贝时陷入死循环,最终导致内存溢出。本文将介绍如何在深拷贝过程中解决循环引用问题。
什么是循环引用
循环引用指的是对象中存在对自身的引用
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.b = obj1;
在上面的代码中,obj1
和 obj2
互相引用,形成了一个循环引用的结构。这个结构会导致深拷贝时出现死循环,最终导致内存溢出。
deepClone
我们先来回顾一下deepClone,常见的实现方式有以下两种:
- 直接用JSON.stringify(obj),这样的缺点是无法复制引用类型
let obj = {
a: 1,
b: [1, 2, 3],
}
let res = JSON.stringify(obj);
console.log(JSON.parse(res));
- 第二种就是常见的手写题了
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
上面两种方法都存在一个问题,就是他们没对循环引用的对象做任何处理,这也是我们今天要讨论的主题。
循环引用
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.b = obj1;
console.log('obj1', obj1);
JSON.stringify(obj1); // 会直接报错
这段代码中我们做了以下步骤
- 创建两个空对象,分别叫做obj1和obj2
- 将obj1的a属性指向obj2,将obj2的b属性指向obj1
- 打印obj1并展开,你会发现出现如下情况
- 如果我们尝试JSON序列化obj1对象,此时你会发现直接报错
上面的示例中,obj1和obj2这两个对象循环引用了,这会导致JS的引擎无法进行垃圾回收,因为引擎判断obj1和obj2都被其他对象引用了。如果我们递归的便利一个被循环引用的对象,那将会直接导致爆栈(stack overflow)。
如何判断一个对象是否被循环引用
可以使用递归 + 哈希表的方式,核心思路是,当我们遍历一个obj的时候,如果map中不存在这个key,就将这对key-val放入map中,如果map中存在这个key,就说明该对象内存在循环引用的情况
function isCircular(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return false
if (hash.has(obj)) return true
hash.set(obj, true)
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (isCircular(obj[key], hash)) return true
}
}
return false
}
WeakMap
WeakMap
是ES6中新引入的数据类型,和Map
类似,它对于值的引用不计入垃圾回收机制的。所以名字中才会有个Weak
,表示这是弱引用。
WeakMap有以下几个特点:
- 不可枚举:因此无法通过 for…of 或 Object.keys() 等方法遍历
- 只接受对象作为键:key只能是
Object
类型,不接受原始类型的值或者其他类型的对象 - 弱引用:key都为弱引用,即如果一个键不再被引用,那么这个键值对会被自动删除,这样就可以避免内存泄漏的问题
- 没有 size 属性:WeakMap 没有 size 属性,因此无法知道 WeakMap 中有多少个键值对
- 不能遍历:由于 WeakMap 中的键值对不可枚举,因此也不能遍历 WeakMap 中的所有键值对
总之,WeakMap 是一种特殊的映射表,用于存储对象之间的关系。由于它的键是弱引用的,因此可以避免内存泄漏的问题。但是,由于它的一些特殊限制,也使得它不能像普通的对象或 Map 一样被广泛地使用。但是,用于判断循环引用,WeakMap 非常适合。
const cache = new WeakMap()
function compute(obj) {
if (cache.has(obj)) {
console.log('Cache hit')
return cache.get(obj)
} else {
console.log('Cache miss')
const result = obj.x + obj.y
cache.set(obj, result)
return result
}
}
const obj1 = { x: 1, y: 2 }
const obj2 = { x: 3, y: 4 }
console.log(compute(obj1)) // Cache miss, 3
console.log(compute(obj1)) // Cache hit, 3
console.log(compute(obj2)) // Cache miss, 7
console.log(compute(obj2)) // Cache hit, 7
解决方案
为了解决这个深拷贝中循环引用的问题,我们需要在拷贝过程中记录已经拷贝过的对象,以便在遇到循环引用时能够正确处理。一种常见的解决方法是使用一个哈希表来存储已经拷贝过的对象,这样我们可以在遇到循环引用时直接返回已经拷贝过的对象的引用。
function deepClone(obj, hash = new WeakMap()) {
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (obj === null || typeof obj !== 'object') return obj
if (hash.has(obj)) return hash.get(obj)
const cloneObj = new obj.constructor()
hash.set(obj, cloneObj)
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloneObj[key] = deepClone(obj[key], hash)
}
}
return cloneObj
}
总结
在进行对象深拷贝时,循环引用是一个常见的问题。为了解决这个问题,我们需要在深拷贝过程中判断对象是否存在循环引用,并使用哈希表来存储已经拷贝过的对象。WeakMap
是一种特殊的映射表,用于存储对象之间的关系,可以避免内存泄漏的问题。