js对象深拷贝

时间:2022-12-05 19:53:39

昨天在工作中遇到了js对象深拷贝的问题,小看了一下,分享出来。

 

什么是深拷贝?

  深拷贝是对应浅拷贝而言的,是引用类型的特有性质。因为对于基本类型来说,任何一个基本的为变量赋值操作都会为新变量开辟一块新的区域,这样来说的话,任何一个一次拷贝都是

深拷贝。如下代码:

var b = "i am zera";
var a = b;

  系统会为a开辟一块新的区域。

  但对于Object,Array,Date,Regex,Function类型来说,如果直接赋值的话,如下所示:

var a = {a:1,b:2,c:3};  //这里a是Object类型,也可以是Array等类型 var b = a;

  则b和a引用的是同一块代码区域,修改了b的同时也会影响到a,这就是浅拷贝。而深拷贝是要模拟基本类型,实现深度的拷贝,生成两个完全不相关的对象。

 

如何实现深拷贝?
  有一种非常简便的实现深拷贝的方式,如下代码所示:

function deepCopy(o){
    return JSON.parse(JSON.stringify(o));
}
var a = {a:1,b:2,c:3};
var b = deepCopy(a);
b.a = 4;
alert(a.a); //1        
alert(b.a); //4

  这种方式很好理解,对一个Object对象而言,先使用内置的JSON.stringify()函数,将其转化为数组。此时生成的字符串已经和原对象没有任何联系了,再通过JSON.parse()函数,将生成的字符串转化为一个新的对象。

而在新对象上的操作与旧对象是完全独立的,不会相互影响。如上面代码所示,将b.a赋值为4,不会影响到a对象,a.a仍是1。这种方法的优点就是简单易懂,站在巨人的肩膀上,使用js内置函数来实现,不需要太大的开销。

而这种方式有一个非常致命的问题,它只能对Object对象实现深拷贝,对于Function等对象,JSON.stringify()函数会直接返回undefined。因为现在的Object对象里面基本都会有Function类型存在(如果没有Function存在,也没有

拷贝的必要了)。那对于有Function类型存在的对象,我们又该怎么去实现呢?那就要看下面这种方法了,这种方法是在网上看到的,代码如下所示:

function getType(o){
    return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase();
}
function extend(destination,source){
    for(var p in source){
        if(getType(source[p])=="array"||getType(source[p])=="object"){
            destination[p]=getType(source[p])=="array"?[]:{};
            arguments.callee(destination[p],source[p]);
        }else{
            destination[p]=source[p];
        }
    }
}
var test={a:"ss",b:[1,2,3],c:{d:"css",e:"cdd"}};
var test1={};
extend(test1,test);
test1.b[0]="change"; //改变test1的c属性对象的d属性
alert(test.b[0]);  //不影响test,返回1
alert(test1.b[0]); //返回change

  这里用到了递归的实现方式,先判断对象是Object还是Array,因为这两个在递归下一次是需要先新建空间的(Array是[],Object是{})。如果是,则创建空间之后在进行递归操作,如果不是,则直接赋值。

这种方法对Object中的Function类型依然适用。

 

需要强调的:

  这里面有一点需要强调一下,是一个写代码习惯的问题,这对我们进行深度的价值有着决定性的意义。在编写代码的过程中,我们应该尽量少的使用对象的名字进行引用,最好使用this(Object),或者是arguments.callee(Function)。具体原因先看以下代码:

var test={a:"ss",b:[1,2,3],c:{d:"css",e:"cdd"},d:function(a){
    test.a = a;
}};
var test1={};
extend(test1,test);
test.d("11");
test1.d("22");
alert(test.a);  //22  
alert(test1.a);    //ss

  很明显,这不是我们希望的结果,我们本来的意思是在test.a的时候输出11,在test1.a的时候输出22。但是,很不幸,事与愿违。原因很简单,那是因为在test中的d函数有对test名字的引用,这在test1中仍然有效,所以后赋值的test1就将先赋值的test中的a覆盖了,而自己的a却没有发生改变。

解决这个问题的也很简单,就是将代码中用红色标出来的test改成this,这样在test1也会是对自身的引用了。

效率问题:

  因为第二种方法是用递归实现的,所以其效率比较低下。而第一种方式是使用js内置函数实现的,开销很小。当然,这只是猜测,用代码测试一下,分别执行100000次深拷贝操作。结果如下所示:

var test={a:"ss",b:[1,2,3],c:{d:"css",e:"cdd",f:{g:"zz"}}};
var test1={};
var start = (new Date()).getTime();
for(var i = 0 ; i < 100000 ; i++){
    extend(test1,test);
}
var end = (new Date()).getTime();
$alert("you have spend1 "+(end - start) + "ms");

var start = (new Date()).getTime();
for(var i = 0 ; i < 100000 ; i++){
    test1 = deepCopy(test);
}
var end = (new Date()).getTime();
$alert("you have spend2 "+(end - start) + "ms");

 结果输出:

  you have spend1 627ms
  you have spend2 254ms
这也验证了我们之前的猜测。因此,如果是简单的深拷贝,就使用第一种方法,而如果Object中包涵Function类型,则选择第二种方法。
 
OVER