关于JavaScript深浅拷贝的理解

      上一篇内容提到了基本类型值和引用类型值在复制,传递时的不同表现。由此也引出了这篇文章的内容--深浅拷贝。这处就有了三个概念:赋值,浅拷贝,深拷贝。
      首先,我们说到的深浅拷贝是针对引用类型的。基本类型值复制后得到的是两个互不相关的独立个体,一个值的改变不会影响到另一个。而引用类型复制后,两个变量保存的都是指向同一个对象的“指针”。基于这个特性,也才会引出深浅拷贝的话题。那么,字面上理解浅拷贝是否就是引用类型通过引用复制得到的结果了?例如下面这样的:
var obj = {
    name : "jack"
};
var copy_obj = obj;     //copy_obj  是否就是我们所说的浅拷贝?

      带着这个疑问,先查一下深浅拷贝的定义,所谓浅拷贝就是新增加一个“指针”指向已有的对象,深拷贝就是在浅拷贝的基础上,新开辟一个内存空间,使新增的“指针”指向新的内存空间保存的对象。类比下就是浅拷贝相当于起了个小名,深拷贝就是多莉羊。根据上面的叙述,似乎copy_obj 就是obj的浅拷贝。

       暂时先这么理解着,接下来思考如何实现深拷贝。对,遍历对象啊。把key-value逐个取出来放到一个新对象中,我们先试一下:

var obj = {
    name : 'jack',
    age : 12,
    score : [1,2,3]
};
var copy_obj = {};
(function copy(obj){
    for(var k in obj){
        if(obj.hasOwnProperty(k))
        copy_obj[k] = obj[k];
    }
})(obj);

这样看起来我们应该是得到两个对象了,下面做一下测试:

obj.age = 13;
copy_obj.age        //12
obj.score.push(4);
copy_obj.score      //[1,2,3,4]

也就是说,我们上面做的遍历赋值只是在 var copy_obj  = obj 的基础上前进了很小的一步。再回顾一下:

var obj = {
    name : "jack"
};
var copy_obj = obj;
obj.name = 'Alice';
copy_obj.name       //Alice

所以,两者的区别在于,我们的function确实生成了一个新对象,但是新对象copy_obj中value为引用类型的值的指向和原对象obj中的指向是一致的。也就是说:

obj.score === copy_obj.score

由此,我们对之前关于赋值和浅拷贝的疑问有了一个答案,结果就是引用类型的赋值是浅拷贝中的一种。再去思考深拷贝的实现,上面写的function已经完成了创建一个新对象,缺少的就是让对象中的“子对象”也独立出来。思路有了,我们需要对“子对象”也进行遍历。因此,我们遇到了一个新问题,如何判断对象中value的类型。首先,我们看下Js内置的引用类型:

  • Object
  • Array
  • Date
  • RegExp
  • Function
  • 基本包装类型

其中“基本包装类型”在我们这里是不用考虑的。在上一篇关于Js变量的文章里面我们提到了一个方法:

Object.prototype.toString.bind(obj)().slice(8, -1);

做个测试:

var obj = {
    a : {},
    b : [1,2],
    c : new Date(),
    d : /^a$/,
    e : function(){
        console.log('hello');
    }
}
function valueType(obj){
    var result = [];
    for(var k in obj){
        if(obj.hasOwnProperty(k)){
            result.push(Object.prototype.toString.bind(obj[k])().slice(8, -1));
        }
    }
    return result;
}
var result = valueType(obj);  //["Object", "Array", "Date", "RegExp", "Function"]

OK,第一步解决。然后想一下对每种类型怎么处理,如果是Object,我可以新建一个var a = {},然后对内部再遍历;如果是Array,可以新建一个var b = [],然后再遍历放值;如果是Date,先看下obj.c是什么:

obj.c   //Wed Apr 04 2018 10:41:22 GMT+0800 (中国标准时间)
既然是对象,那就有toString()方法,我们先让它以字符串形式返回,然后以其作为参数,再去创建一个Date,大概就是下面这样:
var str = obj.c.toString();
var newDate = new Date(str);
newDate   //Wed Apr 04 2018 10:41:22 GMT+0800 (中国标准时间)
同样的,对于RegExp,是不是都能这样,尝试一下:
var reg = obj.d.valueOf();
var newReg = new RegExp(reg);
newReg               //  /^a$/
newReg == obj.d      //  false

没有问题,那Function呢,在js中,new Function(arg1,arg2,....,function_body),前面argx是函数参数,function_body是函数主体,并且都得是字符串形式,好像实现起来有点麻烦啊,强行去这么操作也是可以实现的,那就要拼接字符串了,暂且先不考虑它了吧(留下了没技术的眼泪。。。)

现在可以汇总一下了,把上面的内容整合一下,试着写一个深拷贝方法,直接贴代码:

var obj = {
    a : {},
    b : [1,2,new Date(),{},/^a$/],
    c : new Date(),
    d : /^a$/,
    e : function(){
        console.log('hello');
    }
};

function deepClone(obj){
    //拷贝对象
    var copy_obj;
    //判断类型
    var type = Object.prototype.toString.bind(obj)().slice(8, -1);
    //Object
    if(type === 'Object'){
        copy_obj = {};
    }else if(type === 'Array'){
        copy_obj = [];
    }else if(type === 'Date'){
        copy_obj = new Date(obj.valueOf());
    }else if(type === 'RegExp'){
        copy_obj = new RegExp(obj.valueOf());
    }else{
        //如果是基本类型值或者type === 'Function'
        return obj;
    }

    //遍历
    if(type === 'Object'){
        for(var k in obj){
            if(obj.hasOwnProperty(k)){
                copy_obj[k] = deepClone(obj[k]);
            }
        }
    }else if(type === 'Array'){
        var i = 0,
            len = obj.length;
        for(;i

验证下得到结果:

obj.a == copy_obj.a              //false
obj.b == copy_obj.b              //false
obj.b[2] == copy_obj.b[2]        //false
obj.b[3] == copy_obj.b[3]        //false
obj.b[4] == copy_obj.b[4]        //false
obj.c == copy_obj.c              //false
obj.d == copy_obj.d              //false
obj.e == copy_obj.e              //true

和前面说的一样,只有Function没有完全独立出来。这个方法暂时就到这里。

我查找深拷贝资料的时候看到提及比较多的就是JSON反解析。具体方法如下:

var obj = {
    a : {},
    b : [1,2,new Date(),{},/^a$/],
    c : new Date(),
    d : /^a$/,
    e : function(){
        console.log('hello');
    }
};
var copy_obj = JSON.parse(JSON.stringify(obj));
copy_obj        //{a: {…}, b: Array(5), c: "2018-04-04T04:30:07.954Z", d: {…}}

结果是Chrome的给出来的,我们可以看到,JSON反解析存在以下几个问题:

  1. Date类型得到的结果是字符串形式
  2. RegExp类型得到的结果是一个空对象
  3. Function类型没办法转换,直接被去掉了

我们看下过程,第一步把对象转换成JSON对象:

var obj = {
    a : {},
    b : [1,2,new Date(),{},/^a$/],
    c : new Date(),
    d : /^a$/,
    e : function(){
        console.log('hello');
    }
};
JSON.stringify(obj)      //    "{"a":{},"b":[1,2,"2018-04-04T04:34:29.667Z",{},{}],"c":"2018-04-04T04:34:29.667Z","d":{}}"

可以看出来在这一步就已经出问题了,这也是用JSON做深拷贝的局限性,如果我们需要深拷贝的对象不包含Date,RegExp,Function,用JSON反解析就很方便了。反之还是一个一个检查类型做出相应的操作从而实现深拷贝。

关于深拷贝,还有一些其他方法,在这就不展开了,上面文字的叙述顺序就是我当时思考深拷贝这个问题时的大体思路。记录下来供大家参考,欢迎大家指正交流。



你可能感兴趣的:(关于JavaScript深浅拷贝的理解)