javascript深拷贝实现

·  阅读 319

由于js中复制引用类型不会重新拷贝其值,而是复制一个引用地址,共享同一份数据,虽然节省了内存空间,但也存在一些问题,如下。

const obj = {a: 1, b: 2};
// 此时copy的值为{a: 1, b: 2}
const copy = obj;

obj.a = 22;

// 此时obj和copy的值都为{a: 22, b: 2},因为他们共享同一份数据
console.log(obj, copy);
复制代码

因此,有时我们希望拷贝引用类型数据时,是重新创建一份新的数据,而不是拷贝一个引用地址。目前主要有两种方案:1.JSON.parse(JSON.stringify(data));2.使用递归的方式实现。

使用JSON.parse(JSON.stringify())方式实现深拷贝的优缺点

优点

方便快捷、简单明了。

缺点

  1. 只能拷贝四种值,字符串、数值(十进制)、布尔值和null(NaN, Infinity, -Infinity和undefined都会被转为null);
  2. 对包含循环引用的对象(对象之间相互引用,形成无限循环)使用此方法,会抛出错误。

使用递归实现深拷贝方式的优缺点

优点

  1. 支持所有的js数据类型;
  2. 支持包含循环引用的对象(对象之间相互引用,形成无限循环);
  3. 可扩展性和可选择性高。

缺点

代码实现较为复杂,晦涩难懂。

由浅入深实现深拷贝

1.先实现一个简单的深拷贝。

代码如下:

const deepClone = (data) => {
  // 简单数据类型直接返回值
  if (!(data instanceof Object)) {
    return data;
  }

  const newObj = Array.isArray(data) ? [] : {};

  for (const key in data) {
    // 简单数据类型直接复制值
    if (!(data[key] instanceof Object)) {
      newObj[key] = data[key];
      continue;
    }

    // 复杂数据类型递归
    newObj[key] = deepClone(data[key]);
  }

  return newObj;
};

const obj = {
  a: 1,
  b: 2
};

const newObj = deepClone(obj);

obj.a = 10;
console.log(obj, newObj);
复制代码

以上代码实现了一个简单的深拷贝,但也存在一些严重的问题。如果传入一个包含循环引用的对象,则会造成栈溢出。 循环引用:对象之间相互引用,形成无限循环。如下所示:

const obj = {a: 1};
const copy = {b: 2};

// obj对象的新属性b引用copy对象
obj.b = copy;
// copy对象的新属性a引用obj对象
copy.a = obj;
复制代码
2.解决循环引用的问题

我们可以用一个变量保存拷贝过的复杂数据类型的引用地址,然后在每次拷贝复杂数据类型之前,先判断是否拷贝过了。代码如下:

const deepClone = (obj) => {
  // 用来保存引用关系,解决循环引用问题(使用闭包私有化hasObj变量)
  // 可以weakMap弱引用来保存,这里为了兼容使用object
  const hasObj = {};

  const clone = (data) => {
    // 简单数据类型直接返回值
    if (!(data instanceof Object)) {
      return data;
    }

    const newObj = Array.isArray(data) ? [] : {};

    for (const key in data) {
      // 简单数据类型直接复制值
      if (!(data[key] instanceof Object)) {
        newObj[key] = data[key];
        continue;
      }

      // 判断是否拷贝过该对象,解决循环引用的问题
      if (hasObj[key] === data[key]) {
        newObj[key] = data[key];
        continue;
      }

      // 保存引用
      hasObj[key] = data[key];

      // 复杂数据类型 递归
      newObj[key] = clone(data[key]);
    }

    return newObj;
  }

  return clone(obj);
};
复制代码

以上代码,虽然解决了循环引用的问题,但是还有一些问题,例如:不能拷贝函数、不能拷贝date对象、不能拷贝正则对象等问题。

3.解决不能拷贝特殊数据类型的问题

我们可以对特殊数据类型做单独处理。完整代码如下:

/**
 * @description 深拷贝实现(支持Map、Set、RegExp、Date、Function类型,支持循环引用)
 * @param {object} obj 需要深拷贝的对象
 * @returns 返回一个新对象
 */
const deepClone = (obj) => {
  // 用来保存引用关系,解决循环引用问题(使用闭包私有化copyObj变量)
  // 可以weakMap弱引用来保存,这里为了兼容使用object
  const copyObj = {};

  const clone = (data) => {
    // 简单数据类型直接返回值
    if (!(data instanceof Object)) {
      return data;
    }

    const newObj = Array.isArray(data) ? [] : {};

    for (const key in data) {
      // 跳过原型上的属性(可以不跳过)
      if (!data.hasOwnProperty(key)) {
        continue;
      }

      // 简单数据类型直接返回值
      if (!(data[key] instanceof Object)) {
        newObj[key] = data[key];
        continue;
      }

      // 拷贝date对象
      if (data[key] instanceof Date) {
        newObj[key] = new Date(data[key].getTime());
        continue;
      }

      // 拷贝正则对象
      if (data[key] instanceof RegExp) {
        newObj[key] = new RegExp(data[key]);
        continue;
      }

      // 拷贝函数
      if (data[key] instanceof Function) {
        newObj[key] = new Function(`return ${data[key].toString()}`)();
        continue;
      }

      // 拷贝map
      if (data[key] instanceof Map) {
        newObj[key] = new Map();

        data[key].forEach((val, mapKey) => {
          if (!(mapKey instanceof Object) && !(val instanceof Object)) {
            newObj[key].set(mapKey, val);
          } else {
            newObj[key].set(clone(mapKey), clone(val));
          }
        });

        continue;
      }

      // 拷贝set
      if (data[key] instanceof Set) {
        newObj[key] = new Set();

        data[key].forEach((val) => {
          if (!(val instanceof Object)) {
            newObj[key].add(val);
          } else {
            newObj[key].add(clone(val));
          }
        });

        continue;
      }

      // 判断是否为循环引用
      if (copyObj[key] === data[key]) {
        newObj[key] = data[key];
        continue;
      }

      // 保存引用
      copyObj[key] = data[key];

      // 复杂数据类型,递归处理
      newObj[key] = clone(data[key]);
    }

    return newObj;
  };

  return clone(obj);
};
复制代码

以上代码还是有一些问题,不能复制symbol属性和其他的特殊数据类型(如:bigInt),这些问题就留给读者自己解决了。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改