深拷贝

原文来自:https://www.cnblogs.com/Chen-XiaoJun/p/6217373.html

深拷贝的实现方式

要完全复制又不能修改到原对象,这时候就要用 Deep Copy,这里会介绍几种Deep Copy 的方式。

1、手动复制

把一个对象的属性复制给另一个对象的属性

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

但这样很麻烦,要一个一个自己复制;而且这样的本质也不能算是 Deep Copy,因为对象里面也可能是对象,如像下面这个状况:

var obj1 = { body: { a: 10 } };
var obj2 = { body: obj1.body };
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 20 } } <-- 被改到了
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// true

虽然obj1跟obj2是不同对象,但他们会共享同一个obj1.body,所以修改obj2.body.a时也会修改到旧的。

2、对象只有一层的话可以使用:Object.assign()函数

Object.assign({}, obj1)的意思是先建立一个空对象{},接着把obj1中所有的属性复制过去,所以obj2会长得跟obj1一样,这时候再修改obj2.b也不会影响obj1。

因为Object.assign跟我们手动复制的效果相同,所以一样只能处理深度只有一层的对象,没办法做到真正的 Deep Copy。不过如果要复制的对象只有一层的话可以考虑使用它。

2.1 对象只有一层的话可以使用:展开运算符…

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

3、转成 JSON 再转回来

用JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。

var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false

这样做是真正的Deep Copy,这种方法简单易用。

但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。

也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

var obj1 = { fun: function(){ console.log(123) } };
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(typeof obj1.fun);
// 'function'
console.log(typeof obj2.fun);
// 'undefined' <-- 没复制

要复制的function会直接消失,所以这个方法只能用在单纯只有数据的对象。

4、递归拷贝

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    if (typeof initalObj[i] === 'object') {  //数组或对象
      obj[i] = (initalObj[i].constructor === Array) ? [] : {};            
      arguments.callee(initalObj[i], obj[i]);  //递归调用
    } else {
      obj[i] = initalObj[i];
    }
  }    
  return obj;
}

var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);

上述代码确实可以实现深拷贝。但是当遇到两个互相引用的对象,会出现死循环的情况。

为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环。

改进版代码如下:????

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
    if(prop === obj) {            
      continue;
    }        
    if (typeof prop === 'object') {
      obj[i] = (prop.constructor === Array) ? [] : {};            
      arguments.callee(prop, obj[i]);
    } else {
      obj[i] = prop;
    }
  }    
  return obj;
}


var str = {};
var obj = {};
obj.a = obj;
obj.b = 21;
deepClone(obj, str);
console.log(str.a);

5、使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
    if(prop === obj) {            
      continue;
    }        
    if (typeof prop === 'object') {
      obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
    } else {
      obj[i] = prop;
    }
  }    
  return obj;
}

6、jquery

jquery 有提供一个$.extend可以用来做 Deep Copy。

var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false

7、lodash

另外一个很热门的函数库lodash,也有提供_.cloneDeep用来做 Deep Copy。

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

这个性能还不错,使用起来也很简单。

8.数组的slice,concat只能深拷贝一层 要注意

var a=[1,2,3,4],
    b=a.slice();
a[0]=2;
console.log(a,b);   //[2,2,3,4]  [1,2,3,4]  实现深拷贝了

var a=[0,1,[2,3],4],
        b=a.slice();
a[0]=1;
a[2][0]=1;
console.log(a,b);  // [1,1,[1,3],4]  [0,1,[1,3],4] 第二层的还是共用了

所以,数组的slice,concat只能深拷贝一层,与Object.assign()函数类似,不是真正的深拷贝。

ES6数组的深拷贝

第一种:Array.from(要复制的数组);

var arr1=[1,2,3];
var arr2=Array.from(arr1);
arr1.push(4);
alert(arr1);  //1234
alert(arr2);  //123
arr2.push(5);
alert(arr1);  //1234
alert(arr2);  //1235

第二种:…

var arr1=[1,2,3];
var arr2=[...arr1];
arr1.push(4);
alert(arr1);  //1234
alert(arr2);  //123
arr2.push(5);
alert(arr1);  //1234
alert(arr2);  //1235

第二种这个方法也可以用在函数的形参上面。

function show(...arr1){  //直接来复制arguments这个伪数组,让它变成真正的数组,从而拥有数组的方法。
  alert(arr1); //1234
  arr1.push(5);
  alert(arr1); //12345
}
show(1,2,3,4)

深拷贝的问题:

function P(obj){}

P.prototype.caa=function(){console.log(1)};

var p =new RegExp('/[.]/');

var obj = new P();  //

var newObj;

var obj1={a:1,b:2,c:3,d:{a:1,b:2,c:3,d:{a:1,b:2,c:3,d:{a:1,b:2,c:3,d:Date.now(),e:p}}}};//一个多层嵌套对象  

function objCopy(obj){
    var copy_obj={};
    for(var i in obj){
        if(typeof obj[i]=='object'){//如果对应KEY的数据类型是object的话,就进行递归调用
              copy_obj[i]=objCopy(obj[i]);
        }else{
              copy_obj[i]=obj[i];
        }
    }
    return copy_obj;
}

var newObj=objCopy(obj1);

console.log(obj1);

console.log(newObj);
//正则表达式 赋值有问题

var newObj = objCopy(obj);

console.log(newObj);  //{caa:f(){}}  原型上的方法是可以拷贝的。

JS的一些特殊对象,有些结构并不与一般对象结构相同,需要用其他特殊的方式进行特别处理,正则对象就是(另外,如果是一个构造函数所生成的对象,也是可以拷贝到该构造函数的方法的)。

循环引用的问题解决

循环引用时,会递归出错。

var source = {a:1,b:{name:"sonya"}};
    source.xx = source;  //单个属性 循环引用,不会报错
    function objCopy(obj){
        var copy_obj={};
        var index=0;
        for(var i in obj){
            if(index==0){
                obj.copyKey=1;
            }

            if(typeof obj[i]=='object'){ //如果对应KEY的数据类型是object的话,就进行递归调用
                console.log(obj[i]);
                if(!obj[i].copyKey){  //obj[i].copyKey为空,没有循环引用的对象
                    copy_obj[i]=objCopy(obj[i]);
                    delete copy_obj[i].copyKey;
                    delete obj[i].copyKey;
                }else{  //循环引用的情况,直接浅复制
                    copy_obj[i]=obj[i];
                    delete copy_obj[i].copyKey;
                    delete obj[i].copyKey;
                }
            }else{
                copy_obj[i]=obj[i];
            }
            index++;
        }

        return copy_obj;
    }

var res = objCopy(source);
console.log(source);
console.log(res);

如果多次引用,因为这个copyKey的mark,可能在第一次调用的时候被清除了,第二次调用的时候还是会陷入循环引用,产生循环引用的错误,测试了一下,和猜测的一样,于是我又改了一下代码。

var source = {a:1,b:{name:"sonya"}};
source.xx = source; 
source.yy = source;  //多个属性 循环引用

function objCopy(obj,key){
    var copy_obj={};
    var index=0;
    for(var i in obj){
        if(index==0){//添加copyMark
            obj.copyKey=1;
        }
        if(typeof obj[i]=='object'){//如果对应KEY的数据类型是object的话,就进行递归调用
            if(!obj[i].copyKey && key!=i){  //没有循环引用的对象
                copy_obj[i]=objCopy(obj[i],i);//递归调用增加对对象KEY的判断
                delete copy_obj[i].copyKey;//去除copyMark,来自递归调用
                delete obj[i].copyKey;//去除copyMark,来自递归调用
            }else{
                copy_obj[i]=obj[i];
                delete copy_obj[i].copyKey;//去除copyMark,来自赋值
                delete obj[i].copyKey;//去除copyMark,来自赋值
            }
        }else{
            copy_obj[i]=obj[i];
        }
        index++;
    }
    return copy_obj;
}

var res=objCopy(source);
console.log(source);
console.log(res);

原文查看:http://www.24-80.com/javascript/article-97.html

Table of Contents