深拷贝-循环引用的处理

10,552 阅读4分钟

拷贝一个存在循环引用的对象时报错

以下为函数自己调用自己栈溢出报错(环境:浏览器控制台)

(function a() {
    a();
})();
// Uncaught RangeError: Maximum call stack size exceeded

调用堆栈会一直增长,直到达到限制:浏览器硬编码堆栈大小或内存耗尽。为了解决这个问题,请确保您的递归函数具有能够满足的基本情况 以上代码函数a自己调用自己。导致栈溢出.

上方的代码如何解决爆栈问题呢? 有了停止调用的判断条件,就不会有堆栈溢出了

(function a(x) {
    if ( ! x) {
        return;
    }
    a(--x);
})(10);

以下代码,深拷贝一个存在循环引用的对象时。报错。

function deepCopy(obj){
	var res = obj.constructor === Array ? [] :{};
	for(var key in obj){
		if(typeof obj[key] === "object"){
			res[key]=deepCopy(obj[key])
		}else{
			res[key]=obj[key]
		}
	}
	return res;
};

var A={
    a:1,
    b:[1,2,3],
    c:{
        "0":0
    },
    d: undefined,
    e: null,
    f: new Date()
};
A.A=A;
console.log("A",A);
A.A=A;
var B = clone(A)
console.log(A,B)
// Uncaught RangeError: Maximum call stack size exceeded
// 未捕获范围错误 超过最大调用堆栈大小

以上代码A正常输出,当调用deepCopy时报错,栈溢出

报错分析

  • A对象存在循环引用,打印他时不会栈溢出
  • 深拷贝A时,才会导致栈溢出。

深拷贝(解决循环引用时报错)

深拷贝处理(目标对象存在循环引用时报错处理)

从所周知,对象的key不可以是对象,如下:

{{a:2}:1};//Uncaught SyntaxError: Unexpected token ':'

使用Map的key可以是对象的特性。把要拷贝的目标对象,当做Key存起来,value是深拷贝后的对象(在Map中有这样一个键值对)。

function deepCopy(obj, map = new Map()) {
  if (typeof obj === 'object') {
     let res = Array.isArray(obj) ? [] : {};
	 if(map.get(obj)){
		return map.get(obj);
	 }
	 map.set(obj,res);
     for(var i in obj){
		res[i] = deepCopy(obj[i],map);
	 }
	 return map.get(obj);
  }else{
	return obj;
  }
};
var A={a:1};
A.A=A;

var B = deepCopy(A);
console.log(B);//{a: 1, A: {a: 1, A: {…}}

以上代码用的是浏览器控制台,我们看到可以把循环引用依次展开。不会报错。

以上代码在node中的输出如下:

{ a: 1, A: [Circular] }

我们看到,Circular 代表A的属性循环引用了自身.A的属性等于输入对象A自己。 通过Circular直接告知是循环引用自身。

浏览器和node输出的区别

  • 浏览器会正常输出处理完的循环引用对象。
  • node通过cirular来标识是循环引用。

用循环引用的deepCopy版本,拷贝正常的对象时。

function deepCopy(obj, map = new Map()) {
  if (typeof obj === 'object') {
     let res = Array.isArray(obj) ? [] : {};
	 if(map.get(obj)){
		return map.get(obj);
	 }
	 map.set(obj,res);
     for(var i in obj){
		res[i] = deepCopy(obj[i],map);
	 }
	 console.log("map",map);
	 return map.get(obj);
  }else{
	return obj;
  }
};
var A = {a:1,b:[1,2,3]};
var B = deepCopy(A);
console.log(B); //{a:1,b:[1,2,3]}

以上代码打印出来的map是什么呢?

[ [{a:1,b:[1,2,3]},{a:1,b:[1,2,3]}] , [[1,2,3],[1,2,3]] ]

我们可以看到map中存了两个引用类型的key和value一样的数据,包括目标对象。 就好比,你有一个对象

 var A={a:1}; A.A=A; A.B=A;
    

我们可以看到,A上有一个属性A就是A对象。那么,map中已经有一个key,value一样的A对象。你的属性A的值在map中有,那么,就直接返回已经有的这个对象A。也就是对象A自身的引用。

如果你输入数据是[1,2,3],那么map中将保存的是[[1,2,3],[1,2,3]] 正因为如此,我们才能利用map来解决循环引用的问题

1) 解决循环引用

//使用Map函数
function deepCopy(obj,map = new Map()){
    if (typeof obj != 'object') return 
    var newObj = Array.isArray(obj)?[]:{}
    if(map.get(obj)){ 
      return map.get(obj); 
    } 
    map.set(obj,newObj);
    for(var key in obj){
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] == 'object') {
                newObj[key] = deepCopy(obj[key],map);
            } else {
                newObj[key] = obj[key];
            }
        }
    }
    return newObj;
}
const obj1 = {
        x:1,
        y:2,
        d:{
            a:3,
             b:4
         }
    }
  obj1.z = obj1;
  const obj2 = deepCopy(obj1);
  console.log(obj2)
            
//node 输出{ x: 1, y: 2, d: { a: 3, b: 4 }, z: [Circular] }
//控制台输出{x: 1, y: 2, d: {…}, z: {…}}

总结:

  • return map.get(obj); 返回这个和返回res是一样的。因为在map中保存的数据,是key和value一模一样的键值对, 包括拷贝的目标对象。
  • Map是如何解决循环引用的?

是通过存储键值对一样的对象。包括你深拷贝最终返回的对象。就是说你Map中有一个键值对就是key是目标对象,value也是目标对象。当有循环引用,递归调用时,就会加一个条件,如果map中有这个对象的话,直接返回这个对象。前提是,每一次递归的时候,我们保存了这个对象为key,value也为这个对象的键值对在Map中。

互相交流学习

大家也可以关注我的GitHub,互相交流学习进步~