songning0605 / blog

整理记录
1 stars 0 forks source link

深拷贝和浅拷贝 #25

Open songning0605 opened 4 years ago

songning0605 commented 4 years ago

参考

深拷贝和浅拷贝的定义

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

image

常用方法

function clone(target) {
  let cloneTarget = {};
  for (const key in target) {
    cloneTarget[key] = target[key];
  }
  return cloneTarget;
}

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

image

常用方法

递归深拷贝

JSON.parse(JSON.stringify(obj)) 虽然好用,但是存在很明显的缺陷,所以需要使用递归来完善深拷贝,由于事先无法知道拷贝深度,可以用递归来解决问题,在浅拷贝的基础上进行如下修改:

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
  const targetType = typeof target;
  if (targetType === "object") {
    let cloneTarget = {};
    for (const key in target) {
      cloneTarget[key] = clone(target[key]);
    }
  } else {
    return target;
  }
}

这段代码在做深拷贝时,只考虑了简单的 Object 类型,没有考虑数组等其他对象类型的拷贝,需要继续完善。

考虑数组

function clone(target) {
  const targetType = typeof target;
  if (targetType === "object") {
    let cloneTarget = Array.isArray(target) ? [] : {};
    for (const key in target) {
      cloneTarget[key] = clone(target[key]);
    }

    return cloneTarget;
  } else {
    return target;
  }
}

循环引用

为了解决循环引用问题,可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就解决了循环引用的问题。

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 WeakMap 这种数据结构:

选择 WeakMap,而不选择 Map,是因为 WeakMap 的 key 是弱引用类型,会自动释放,Map 是强引用类型,不会自动释放,当拷贝对象比较大的时候会造成内存浪费

function clone(target, map = new WeakMap()) {
  if (typeof target === "object") {
    let cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
      cloneTarget[key] = clone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

其他数据类型

在上面的代码中,我们其实只考虑了普通的 object 和 array 两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。

获取数据类型

定义一个工具函数:

function getType(target) {
  return Object.prototype.toString.call(target);
}

返回结果如下: image

也可以定义一些常量:

const mapTag = "[object Map]";
const setTag = "[object Set]";
const arrayTag = "[object Array]";
const objectTag = "[object Object]";

const boolTag = "[object Boolean]";
const dateTag = "[object Date]";
const errorTag = "[object Error]";
const numberTag = "[object Number]";
const regexpTag = "[object RegExp]";
const stringTag = "[object String]";
const symbolTag = "[object Symbol]";

上面的引用类型,可以简单的分为两类:

可以继续遍历的类型和不可以继续遍历的类型需要分开进行拷贝。

可继续遍历的类型

上面我们已经考虑的 object、array 都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有 Map,Set 等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型。

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]和{},我们可以通过拿到 constructor 的  方式来通用的获取。

例如:const target = {}就是 const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

function getInit(target) {
  const Ctor = target.constructor;
  return new Ctor();
}

下面,改写 clone 函数,对可继续遍历的数据类型进行处理:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

const deepTag = [/*...types*/];

function clone(target, map = new WeakMap()) {
  // 克隆原始类型
  if (!isObject(target)) {
    return target;
  }

  // 初始化
  const type = getType(target);
  let cloneTarget;
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target, type);
  }

  // 防止循环引用
  if (map.get(target)) {
    return map.get(target);
  }
  map.set(target, cloneTarget);

  // 克隆set
  if (type === setTag) {
    target.forEach((value) => {
      cloneTarget.add(clone(value, map));
    });
    return cloneTarget;
  }

  // 克隆map
  if (type === mapTag) {
    target.forEach((value, key) => {
      cloneTarget.set(key, clone(value, map));
    });
    return cloneTarget;
  }

  // 克隆对象和数组
  const keys = type === arrayTag ? undefined : Object.keys(target);
  forEach(keys || target, (value, key) => {
    if (keys) {
      key = value;
    }
    cloneTarget[key] = clone(target[key], map);
  });

  return cloneTarget;
}

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:

Bool、Number、String、String、Date、Error 这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型:

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

完整代码

image