javascript深度拷贝分析

时间:2022-10-30 19:52:41

先来介绍一下深拷贝是如何而来的。

深拷贝是对比与浅拷贝而言的,我们都知道js中分为基本类型以及引用类型,基本类型都是保存在栈里面的,而引用类型仅仅是把地址保存在栈里面,而值是存在于堆里面,浅拷贝仅仅是拷贝栈里面东西;

也就是说对于基本类型而言在栈里面重新开辟一个空间保存复制后的值和址,而对于引用类型而言仅仅是把地址复制过来,而这个地址指向的依然是原来址指向的那个堆里面的值,这样一来如果我们浅复制的话对于基本类型而言是没问题的,

而对于引用类型来说 就相当于两个地址同时指向了一个值,修改两个地址里面任意一个的值都会对另一个造成影响。看一段代码:

var a={

  name:'yuchao',

  age:'26'

}

var b=a;

b.sex="man";

console.log(a);//{name: "yuchao", age: "26", sex: "man"}

console.log(b);//{name: "yuchao", age: "26", sex: "man"}

可以看到输出的是同样结果,一般情况下这都不是我们想要的结果,所以我们引进了能将对象等引用类型重新指向的深拷贝。

如何实现深度拷贝

对于实现深拷贝方法很多,我们来慢慢看:

1.json转化

对于高级浏览器(ie9+firefox、chrome等)里面有内置的JSON对象以及stringify和parse方法对字符串以及object之间进行互相转化。

这个转化过程我们可以加以利用,因为每一次转化都是重新定义一个新空间。这种方式很好理解,对一个Object对象而言,先使用内置的JSON.stringify()函数,将其转化为json字符串。

此时生成的字符串已经和原对象没有任何联系了,再通过JSON.parse()函数,将生成的字符串转化为一个新的对象。

而在新对象上的操作与旧对象是完全独立的,不会相互影响。实现代码也是简单易懂,并且利用js内置函数不需要过多内存开销。

function deepCopy(json){

  return JSON.parse(JSON.stringify(json));

}

举例:var person={name:'yuchao',age:'26',getName:function(){alert(this.name)}};

var newPerson=deepCopy(person);

newPerson.name='xiaopu';

console.log(person);//Object {name: "yuchao", age: "26",getName:function(){alert(this.name)}}

console.log(newPerson);//Object {name: "xiaopu", age: "26"}

在上面代码中,我们可以看到通过深拷贝newPerson以及跟person两个对象毫无关系了,修改newPerson的值对原对象没有造成任何影响。这对于简单的深拷贝是没有问题的。不过我们都注意到了newPerson中的getName去哪里了?

原因是通过json的stringify以及parse对对象进行转化一般来说都是没问题的,如果我们的对象中包含function类型的话 就会出现问题了。因为stringify在转化function时候就会返回丢失后的json串,那自然parse转化后就会看不到我们的getName了。

貌似是通过这种方法进行深拷贝无法进行,其实不然。我们只需要解决JSON的“拷贝丢失”问题即可。

大家都会使用JSON的stringify以及parse方法,但是可能都没有注意过这两个方法不仅仅可以接受一个对象参数,拿stringify来看ECMAScript中明确表明使用方法JSON.stringify(value [, replacer] [, space])我们在平时都会用第一个value参数,

后两个参数完全忽略掉。而根据介绍:

value是必须的(通常是一个对象或者数组);

第二个参数可选,表示一个函数或数组转换结果。

如果第二个参数是一个函数,调用JSON.stringify函数,通过遍历每个成员的键和值。键被保存,返回值将被使用,而不是原来的值。如果该函数返回未定义,成员被排除。返回就为空。

第三个参数是处理返回空格用,在此不做过多讲解。

我们看到第二个参数用法,第二个参数如果是函数的话 就会遍历源对象中的键值,并且处理键值结果。这样一来我们就可以在遍历中判断源对象中的某个值是否是function然后手动将其转化为字符串来达到我们转化结果;

同理在parse方法中也有第二个参数对源对象进行遍历操作方法。跟stringify使用类似,只是我们判断的不是function类型(字符串无法判断)而是判断值中是否有function字段。那么根据这规则我们实现改进后的深拷贝代码如下:

function deepCopy(json){

    return

    JSON.parse(JSON.stringify(json,function(key,val){

            if(typeof val=='function'){

                return val+''//手动将其字符串化

            }

            return val;

        }

    ),function(key,val){

        if(val.indexOf&&val.indexOf('function')>-1){

            return eval("(function(){return "+val+" })()")

        }

        return val;

    });

}

这样一来我们就可以得到我们想要的真正深拷贝结果了,也就解决了‘拷贝丢失’问题。

var newPerson=deepCopy(person);

newPerson.name='xiaopu'

console.log(person);//Object {name: "yuchao", age: "26",getName:function(){alert(this.name)}}

console.log(newPerson);//Object {name: "xiaopu", age: "26",getName:function(){alert(this.name)}}

利用这种方法可以对所有对象进行深拷贝包括function类型以及array类型。

不过JSON方法本身就是个问题,因为在ie8标准模式以下版本都没有这个对象,如果我们非得想用这个方法进行深度克隆 大可以从网上下个支持json的js文件或者自己写一套支持json的js代码也是可行的。

2.深度遍历拷贝

我们来看看另外一种深度克隆方式,这是目前盛传的一种方式,来自网上代码。

function clone(obj){

    var objClone;

    if (obj.constructor == Object){

        objClone = new obj.constructor();

        }else{

            objClone = new obj.constructor(obj.valueOf());

        }

    for(var key in obj){

        if ( objClone[key] != obj[key] ){

            if ( typeof(obj[key]) == "object"){

                objClone[key] = clone(obj[key]);

            }else{

                objClone[key] = obj[key];

            }

        }

    }

    objClone.toString = obj.toString;

    objClone.valueOf = obj.valueOf;

    return objClone;

}

这段代码简单易懂而功能却十分强大,我们来分析一段这段代码:

这段代码其实解决了两个层面上的深度拷贝1是引用类型层次的拷贝,2是深度循环拷贝。当然也不存在对function处理问题。利用json拷贝也不会存在第二个层面问题。哈哈 半斤八两。。

这个方法首先定义一个objClone变量,然后判断传入的obj的构造函数是什么类型。如果是Object则直接新建一个空的Object对象,如果不是则将obj里面的值赋予新建的obj空的构造函数(这样返回的就是一个string对象或者数组或者function等对象)。

举例来说obj="23";objClone则等于{0:'2',1:'3'};如果obj=[1,2];objClone则等于[[1,2]].

我们对obj进行循环,我们假设obj=[1,{name:'yuchao'}];那么objClone就是[[1,{name:'yuchao'}]];objClone只有一个key那就是0,

这个时候obj会循环两次即obj[0]=1,obj[1]={name:'yuchao'},objClone[key]只会循环一次,这个时候objClone[0]=obj=[1,{name:'yuchao'}],

objClone[1]时候就是undefined,无论key值是多少都符合判断条件继续向下执行,obj[0]值是一个数字就会走else条件即objClone[0]重新赋值为1,

obj[1]是一个对象object则会通过typeof验证深度拷贝(这个深度是拷贝层次的深度)这个时候就会将obj的值改为{name:'yuchao'}重新走一遍clone方法。

在重新走之后 返回的值为{name:'yuchao'}赋值给objClone[1],至此objClone=[1,{name:'yuchao'}];最后由于toString以及valueOf方法是内置对象,无法通过for in循环进行赋值,所以只能手动进行赋值。

==================================================================================================================================

ok,两种模式进行深度克隆就说到这,总结一下:对于拷贝来说 两种方法都没有问题,

方法一优点是克隆速度快,不会消耗太多性能,缺点是浏览器兼容问题。

方法二优点是浏览器兼容,也是当今比较流行的克隆方式,缺点就是方法一的优点(因为遍历所以会消耗一些性能);

话说到这儿,选择什么 看你态度吧!!