手写深拷贝,你写全了吗

128 阅读2分钟

写代码第一层境界:完成功能

第二层境界:考虑边界

第三层境界:优化

-- 改编自某面试官的面试标准

手写深拷贝,你可能会想着用递归,写出下面的代码。

function cloneDeep(source) {
  if (typeof source !== "object" || source == null) {
    return source;
  }
  let res = source instanceof Array ? [] : {};
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = cloneDeep(source[key]);
    }
  }
  return res;
}

整个代码的思路是:如果是基础数据类型,直接返回,如果不是,说明是数组或者对象,进行遍历。hasOwnProperty 是为了过滤原型链的属性。因为 for in 会循环原型链的属性,最后递归。

但是你会发现存在很多问题,比如循环引用,map({})、set({})、weakMap({})、weakSet({})、日期(变成字符串)、正则对象({}),这些数据类型变空对象或者字符串,symbol 作为属性会变成 string。

我们首先解决循环引用导致栈溢出的问题。

思路是用缓存,把数据存下来,如果已经存过了,就直接返回。没存过就存起来。这样就不会顺着引用的对象一直找下去。

这里不用 object 也不用 map,用 weakMap,对象有个问题,键不能是个值,只能是 string。weakMap 是弱引用,不会内存泄漏。

改造一下。

function cloneDeep(source, map = new WeakMap()) {
  if (map.get(source)) {
    return map.get(source);
  }
  //基础数据类型
  if (typeof source !== "object" || source == null) {
    return source;
  }

  //和要拷贝的对象保持类一致,例如对象或数组
  const res = new source.constructor();
  map.set(source, res);

  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = cloneDeep(source[key], map);
    }
  }
  return res;
}

接下来把数据类型补上。

function cloneDeep(source, map = new WeakMap()) {
  if (map.get(source)) {
    return map.get(source);
  }
  //基础数据类型
  if (typeof source !== "object" || source == null) {
    return source;
  }
  //正则
  if (source instanceof RegExp) return new RegExp(source);
  //日期
  if (source instanceof Date) return new Date(source);
  //set
  if (source instanceof Set) {
    const newSet = new Set();
    source.forEach((item) => {
      newSet.add(cloneDeep(item));
    });
    return newSet;
  }

  //map
  if (source instanceof Map) {
    const newMap = new Map();
    source.forEach((value, key) => {
      newMap.set(key, cloneDeep(value));
    });
    return newMap;
  }

  const res = new source.constructor();
  //和要拷贝的对象保持类一致,例如对象或数组
  map.set(source, res);

  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = cloneDeep(source[key], map);
    }
  }
  return res;
}

但是 symbol 作为属性识别不了,所以用 Object.getOwnPropertySymbols 识别 symbol 作为属性并拷贝

function cloneDeep(source, map = new WeakMap()) {
  ...
  //处理symbol作为对象属性的情况
  const symbolKeys = Object.getOwnPropertySymbols(source);
  for (const symKey of symbolKeys) {
    res[Symbol(symKey.description)] = cloneDeep(source[symKey]);
  }
  return res;
}

最后优化一下代码,把 Date, Map, Set, RegExp 一起处理,传给构造函数去生成。

function cloneDeep(source, map = new WeakMap()) {
  if (map.get(source)) {
    return map.get(source);
  }
  //基础数据类型
  if (typeof source !== "object" || source == null) {
    return source;
  }
  //特色对象
  const types = [Date, Map, Set, RegExp];
  if (types.includes(source.constructor)) return new source.constructor(source);

  const res = new source.constructor();
  //和要拷贝的对象保持类一致,例如对象或数组
  map.set(source, res);

  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = cloneDeep(source[key], map);
    }
  }

  //处理symbol作为对象属性的情况
  const symbolKeys = Object.getOwnPropertySymbols(source);
  for (const symKey of symbolKeys) {
    res[Symbol(symKey.description)] = cloneDeep(source[symKey]);
  }
  return res;
}

但是这个代码仍然有问题, WeakMap 和 WeakSet 依然解决不了,因为他们不可枚举。