关于 JavaScript 中的深拷贝

@sakila1012 2018-04-13 08:07:51发表于 sakila1012/blog

写在前面

一般而言,js 中的浅拷贝和深拷贝主要针对 object、array 这些更为复杂的数据类型,而在日常使用中浅拷贝使用更为频繁,以及相比于深拷贝,浅拷贝非常容易懂且问题少的多。

浅拷贝

浅拷贝:只是复制了对象属性的引用,如果原对象改变了,目标对象也会跟着改变

let obj1 = {a : 'foo', b : {c : 'bar'} };
let obj2 = Object.assign({}, obj1);
obj2.b.c = 'foo';
console.log(obj1.b.c)

深拷贝

先分别看看 Array 和 Object 自有的方法支持不?

Array

var arr1 = [1,2], arr2 = arr1.slice();
console.log(arr1); //  [1,2];
console.log(arr2); //  [1,2];

//修改
arr2[0] = 3;
console.log(arr1); //  [1,2];
console.log(arr2); //  [3,2];

此时 arr2 的修改并没有影响到 arr1,将 arr1 改成二维数组再来看看:

let arr1 = [1,2,[3,4]],arr2 = arr1.slice();
console.log(arr1); //  [1,2,[3,4]];
console.log(arr2); //  [1,2,[3,4]];

// 修改
arr1[2][1] = 5;
console.log(arr1); //  [1,2,[3,5]];
console.log(arr2); //  [1,2,[3,5]];

arr2 又跟 arr1 一样了,看来 slice 只能修改一维数组。
具备同等特性的还有:concat, Array.from

Object

Object.assign()

let obj1 = {x : 1, y :  1}, obj2 = Object.assign({}, obj1);
console.log(obj1); // {x : 1, y :  1}
console.log(obj2); // {x : 1, y :  1}

//修改
obj2.x = 2;
console.log(obj1); // {x : 1, y :  1}
console.log(obj2); // {x : 2, y :  1}
var obj1 = {
      x : 1,
      y : {
          m : 1
      }
};
var obj2 = Object.assign({},obj1);
console.log(obj1); // {x : 1, y :  {m : 1}}
console.log(obj2); // {x : 1, y :  {m : 1}}

// 修改
obj2 .y.m = 2;
console.log(obj1); // {x : 1, y :  {m : 2}}
console.log(obj2); // {x : 1, y :  {m : 2}}

Object.assign() 只能实现一维对象的深拷贝

JSON.parse(JSON.stringify(obj))

var obj1 = {
      x : 1,
      y : {
          m : 1
      }
};
var obj2 = JSON.parse(JSON.stringify(obj1);
console.log(obj1); // {x : 1, y :  {m : 1}}
console.log(obj2); // {x : 1, y :  {m : 1}}

// 修改
obj2 .y.m = 2;
console.log(obj1); // {x : 1, y :  {m : 1}}
console.log(obj2); // {x : 1, y :  {m : 2}}

JSON.parse(JSON.stringify(obj)) 看起来可以实现二维对象的深拷贝,不过 MDN有句话描述的很清楚。

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

obj1 改造下

var obj1 = {
  x : 1,
  y : undefined,
  z : function(z1, z2) {
    return z1 + z2;
  },
  a : Symbol('foo')
}
var obj2 = JSON.parse(JSON.stringify(obj1);
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}

// 修改
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}

你会发现,在将 obj1 进行 JSON.stringify() 序列化的过程中,y,z,a 被忽略了,也验证了 MDN 文档上的描述。因此,JSON.parse(JSON.stringify(obj)) 使用起来也有局限性。不能深拷贝含有 undefined,function,Symbol 值的对象。不过 JSON.parse(JSON.stringify(obj)) 已经满足我们大部分使用场景了。

深拷贝: 是对象的层级复制,也就是说目标对象和原对象完全不相干。

上面提到的 javascript 自有方法并不能彻底解决 Array,Object 的深拷贝问题,只能使用递归了。

function deepCopy(obj) {
  let result = {};
  let keys = Object.keys(obj),
       key = null,
       temp = null;
  for (let i = 0; i < keys.length; i ++) {
    key = keys[i];
    temp = obj[key];
    // 如果字段的值也是一个对象,则递归调用
    if (temp && typeof temp === 'object' ) {
      result[key] = deepCopy(temp);
    } else {
      result[key] = temp;
    }
  }
  return result;
}

var obj1 = {
    x: {
        m: 1
    },
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};

var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); // {x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); // {x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}

可以看到,递归可以完美地解决了前面的遗留问题,我们也可以使用第三方库:jQuery 的 $.extend 和 loadash 的 _.cloneDeep来解决深拷贝。上面虽然是用 Object 验证,但对于 Array 也同样适用,因为 Array 也是特殊的 Object。

但是还有一个非常特殊的场景:

循环引用拷贝

var obj1 = {
  x : 1,
  y : 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);

此时如果调用刚才的 deepCopy 函数的话,会陷入一个循环的递归过程,从而导致爆栈。jQuery 的 $.extend 也没有解决,解决这个问题:需要判断一个对象的字段是否引用这个对象或这个对象的任意父级即可,修改代码如下:

function DeepCopy(obj, parent = null) {
    // 创建一个新对象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 该字段有父级则需要追溯该字段的父级
    while (_parent) {
        // 如果该字段引用了它的父级则为循环引用
        if (_parent.originalParent === obj) {
            // 循环引用直接返回同级的新对象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp= obj[key];
        // 如果字段的值也是一个对象
        if (temp && typeof temp=== 'object') {
            // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });

        } else {
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);
console.log(obj1); //
console.log(obj2); //

function clone(value, isDeep) {
  if(value === null) return null;
  if(typeof value !== 'object') return value
  if(Array.isArray(value)) {
    if(isDeep) {
      return value.map(item => clone(item, true))    
    }
    return [].concat(value)
  } else {
    if(isDeep) {
      var obj = {};
      Object.keys(value).forEach(item => {
        obj[item] = clone(value[item], true)
      })
      return obj;
    }
    return {...value}
  }
}

var objects = { c: { 'a': 1, e: [1, {f: 2}] }, d: { 'b': 2 } }
var shallow = clone(objects, true)
console.log(shallow.c.e[1]) // { f: 2 }
console.log(shallow.c === objects.c) // false
console.log(shallow.d === objects.d) // false
console.log(shallow === objects) // false

// 另外一种深拷贝实现

function deepClone(data) {
  var temp = type(data) , o, i, ni;
  if(temp === 'array') {
    o = [];
  } else if (temp === 'object') {
    o = {};
  } else {
    return data;
  }
  if(temp === 'array') {
    for (i = 0, ni = data.length; i < ni; i ++) {
      o.push(deepClone(data[i]));
    }
    return o;
  } else if(temp === 'object'){
   for(i in data) {
     o[i] = deepClone(data[i]);
   }
   return o;
  }
}

深拷贝与浅拷贝的概念只存在于引用类型