昨天在工作中遇到了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");
结果输出: