详解JavaScript对象深拷贝
在几乎所有编程语言中,对象都以引用形式保存给变量、复制给其他变量。JavaScript语言也是如此。因此简单的进行赋值操作进行复制仅仅是对对象数据的引用地址进行一个传递,并不会将对象内部的所有属性进行一个完整的复制。也就是说,当修改其中一个对象,另一个变量也会发生改变,因为他们本质上指向了同一个对象引用。
let obj={
a:1
};
let copyObj=obj;
obj.a=2;
//输出2
console.log(copyObj.a);
这种语言特性有时候对开发人员来说是有益的。但有时候,特定的开发场景会要求“完全复制”对象,也就是复制后的对象和复制前的对象完全独立,互不影响。这个过程就被称为“深拷贝”。相对应的,前面那种简单的赋值复制操作就称为“浅拷贝”。
深拷贝是许多JavaScript面试官会问的问题,经常作为手写代码的题目出现在面试场景中。下面本文就将详细解析JavaScript中深拷贝的实现方式。
JSON复制
这种深拷贝方式的原理是使用JSON.parse、JSON.stringify函数,对目标对象进行一次转义之后重新生成来实现复制:
//json拷贝,不能正确复制undefined、NaN、函数等数据类型
function jsonCopy(obj1){
//{"a":[1,2],"b":{"a":"a","b":{"a":1}},"c":true,"d":null}
let obj2=JSON.stringify(obj1);
obj2=JSON.parse(obj2);
return obj2;
}
let a={
a:[1,2],
b:{a:'a',b:{a:1}},
c:true,
d:null,
e:undefined,
f:function(a){return a+1;},
g:NaN
};
let g=jsonCopy(a);
console.log(g);
程序输出如下:
对于常规场景,JSON复制是最简洁的深拷贝写法。但是从上面的示例中可以看到,JSON复制并不能解析a对象的f和g属性。这说明JSON复制不能处理一些特定的数据类型,例如undefined、NaN、function等其他数据类型(具体情况视浏览器而定)。但可以保证的是,JSON复制可以正确处理以下数据类型的对象属性:数组、普通对象、数字、字符串、布尔值。
递归复制
这种深拷贝方式使用一个递归函数去循环读取目标对象的每一个属性,将每一个属性重新赋值给新对象,最终将新对象返回:
//深拷贝
function deepCopy(obj1,obj2={}){
for(let i in obj1){
if(obj1.hasOwnProperty(i)){
if(Array.isArray(obj1[i])){
obj2[i]=[];
deepCopy(obj1[i],obj2[i]);
}else if(typeof obj1[i]==="function"){
obj2[i]=obj1[i];
}else if(obj1[i] instanceof Object){
obj2[i]={};
deepCopy(obj1[i],obj2[i]);
}else{
obj2[i]=obj1[i];
}
}
}
return obj2;
}
let a={
a:[1,2],
b:{a:'a',b:{a:1}},
c:true,
d:null,
e:undefined,
f:function(a){return a+1;},
g:NaN
};
let f=deepCopy(a);
console.log(f);
程序输出如下:
从程序输出结果可以看到,递归复制可以正确解析所有数据类型的对象属性。虽然递归调用会在运行时保留一个函数栈,从而产生一定的内存和计算性能损耗。但是正常情况下,开发人员需要复制的目标对象不会是一个深度很深的对象(通常情况下,大约深度达到100层以后,递归复制会卡顿)。因此递归复制是一种非常好的深拷贝实现方式。
但是有一点需要注意,如果目标对象有一个属性是Function数据类型,那么JavaScript语言本身也没有提供给开发人员太好的方法来“完全复制”一个函数出来,因此无法复制函数。函数的复制仍然是一个引用的传递(当修改其中一个函数的可变属性时,另一个对象的函数属性也会修改)。