深拷贝的实现

浅拷贝和深拷贝

  先用简单的两句话概括深拷贝和浅拷贝的区别吧。
  浅复制:只将对象的各个属性进行一层复制,因此对于引用数据类型而言复制的是对象地址,导致了“牵一发而动全身”。
  深复制:递归复制了所有层级,复制引用数据类型时会开辟新的栈空间,因此两个对象指向了两个不同的地址。

浅拷贝的实现

这里顺道简单罗列下浅拷贝的方法吧,具体的就不过多介绍了。

  • concat 方法浅拷贝数组。
  • slice 方法浅拷贝数组。
  • Object.assign。
  • … 展开运算符。
  • 手动实现。
function shallowClone(target) {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let key in target) {
      if (target.hasOwnProperty(key)) {
          cloneTarget[key] = target[key];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

深拷贝的实现

JSON.parse+JSON.stringify 实现

  我们实现深拷贝一个对象/数组的时候,除了通过递归去拷贝对象/数组中的每一个引用类型外,使用 JSON.parse() + JSON.stringify() 组合也可以实现深拷贝。不过这存在以下几个缺陷:

  1. 如果对象里面有 Date 对象,则转换后的结果中,时间将只是字符串的形式。而不是对象。
let obj = [new Date()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(typeof obj[0]);   // object
console.log(typeof obj1[0]);  // string
  1. 如果对象里有 RegExpErrorSet, Map 对象,则拷贝后只会得到空对象。
let obj = [new RegExp()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);   // [ /(?:)/ ]
console.log(obj1);  // [ {} ]
  1. 在对象中遇到 undefinedfunctionsymbol 时会自动将其忽略,在数组中则会返回 null
  • 在数组中
let obj = [function(){console.log(1)}, undefined, null, Symbol()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);   // [ [Function], undefined, null, Symbol() ]
console.log(obj1);  // [ null, null, null, null ]
  • 在对象中
let obj = {
  a: function(){console.log(1)},
  b: undefined, 
  c: null,
  d: Symbol()
};
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);   // { a: [Function: a], b: undefined, c: null, d: Symbol() }
console.log(obj1);  // { c: null }
  1. 如果对象里有 NaNInfinity-Infinity,则拷贝后的结果会变成 null
let obj = [NaN, Infinity, -Infinity];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);   // [ NaN, Infinity, -Infinity ]
console.log(obj1);  // [ null, null, null ]
  1. 只能拷贝对象的可枚举的自有属性。如果对象中的某个属性是由构造函数生成的,则深拷贝后会丢弃该属性的 constructor
function Person(name) {       
  this.name = name;       
}      
let person = new Person("dangosky");    
let obj = {date: person};      
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);    // { date: Person { name: 'liai' } }   
console.log(obj1);   // { date: { name: 'liai' } }

  1. 如果对象中存在循环引用的情况也无法正确实现深拷贝。
const a = {
  val: 2
};
a.target = a;

递归实现

  再来看看如何用递归逐层拷贝对象属性以实现深拷贝。

function deepCopy(obj) {
  if(typeof obj !== 'object')  return obj;
  let res = obj.constructor === Array ? [] : {};
  for(let key in obj) {
    if(typeof obj[key] === 'object' && obj[key] !== null) {
      res[key] = deepCopy(obj[key]);
    }
    else {
      res[key] = obj[key];
    }
  }
  return res;
}

var obj = {a:1, arr: [{t: 1}, {t: 2}], b: null, c: undefined};
let obj1 = deepCopy(obj);
obj1.arr[1].t = 20;
console.log(obj);    // { a: 1, arr: [ { t: 1 }, { t: 2 } ], b: null, c: undefined }
console.log(obj1);   // { a: 1, arr: [ { t: 1 }, { t: 20 } ], b: null, c: undefined }

  通过上述 deepCopy 递归实现深拷贝已经能正常工作了,但也存在几个缺陷:

  1. 无法拷贝 DateRegExpErrorSet, Map 对象。
  2. 无法拷贝对象属性中的 constructor
  3. 无法解决循环引用的问题。

优化(最终版本)

下面我们来对 deepCopy 优化一下。

  1. 解决循环引用的问题。只需要记录拷贝过的对象,并在拷贝一个对象之前先判断下该对象是否已经拷贝过就行了。这里可以借助 Map 来实现。
function deepCopy(obj, map = new WeakMap()) {
  if (map.get(obj)) {
    return obj;
  }
  if(typeof obj !== 'object')  return obj;
  let res = obj.constructor === Array ? [] : {};
  // 标记 obj 已经拷贝过了
  map.set(obj, true);
  for(let key in obj) {
    if(typeof obj[key] === 'object' && obj[key] !== null) {
      res[key] = deepCopy(obj[key], map);
    }
    else {
      res[key] = obj[key];
    }
  }
  return res;
}

要注意的是,上述代码里使用 WeakMap 而不是 Map。这是因为 Map 上的 keyMap 构成了强引用关系,而 WeakMap 则是弱引用。举个例子说明下什么是强引用和弱引用。(详情可见阮一峰的 ES6 教程

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代码里 arr 使用到了 e1e2 两个对象。如果是强引用的话,则当不需要使用这两个对象的时候,还需要手动删除 arr 对这两个对象的引用(arr[0] = null; arr[1] = null),否则垃圾回收机制就不会释放 e1e2 占用的内存。但如果是弱引用的话,垃圾回收机制则不将该引用考虑在内。因此只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。

考虑到当前这个场景,如果 Map 引用的对象已经不需要用到了,那么这些对象就可以被回收了。使用 Map 因为是强引用,所以还需要手动去释放 Map 对这些对象的引用。但如果使用 WeakMap 的话则是弱引用,WeakMap 对其使用的对象不会计入引用范围内。(WeakMap 的键名只能是 Object

  1. 解决 DateRegExpErrorSet, Map 等数据类型的拷贝问题。需要判断要拷贝对象的数据类型,并获取该数据类型的构造器,以此来构造出一个新的数据。对于 DateRegExpError 可以直接利用它们的指构造出一个新的数据,对于 SetMap 就需要再遍历它们的元素并递归拷贝。
function deepCopy(obj, map = new WeakMap()) {
  if (map.get(obj)) {
    return obj;
  }
  // 如果 obj 只是基本类型的话,就直接返回
  if(typeof obj !== 'object' || obj === null) return obj;
  const objType = Object.prototype.toString.call(obj);
  // 根据 obj 的数据类型获取到它的构造函数
  const constructorFn = Object.getPrototypeOf(obj).constructor;
  // 根据构造器创建不同的数据类型,并注意需要传递 obj 为参数。如果是 Date、Error 等数据类型才可以获取到这个值
  const res = new constructorFn(obj);
  // 标记 obj 已经拷贝过了
  map.set(obj, true);
  if (objType === "[object Array]" || objType === "[object Object]") {
    for(let key in obj) {
      // 因为 in 方法会遍历到 obj 的原型连上,所以需要判 key 是不是 obj 自己的属性
      if (obj.hasOwnProperty(key)) {
        res[key] = deepCopy(obj[key], map);
      }
    }
  } else if (objType === "[object Map]") {
    obj.forEach((item, key) => {
      res.set(deepCopy(key), deepCopy(item));
    })
  } else if (objType === "[object Set]") {
    obj.forEach(item => {
      res.add(deepCopy(item));
    })
  }
  return res;
}

/* Test */
const obj = {
  'a': 1,
  'b': false,
  'c': 'dangosky',
  'd': [1, 2 ,3],
  'e': { 'name': 'dangosky' },
  'f': undefined,
  'g': null,
  'h': new Map([ ['key', 'size']]),
  'i': new Set([1, 2, 3]),
  'j': new Date(1245),
  'k': new Error('error'),
  'l': new RegExp(/dangosky/g),
  'm': function() {console.log(1)},
  'n': () => {console.log(2)}
}
const res = deepCopy(obj);
console.log(obj);
console.log(res);

  转载请注明: DangoSky 深拷贝的实现

  目录