拷贝一个存在循环引用的对象时报错
以下为函数自己调用自己栈溢出报错(环境:浏览器控制台)
(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,互相交流学习进步~