深拷贝(一):基础

164 阅读4分钟

深浅拷贝

浅拷贝:基础数据、对象引用地址,只拷贝第一层 const foo = { ...obj }

深拷贝:拷贝全部,另开空间,不是简单的引用地址

深拷贝

方式

JSON.parse(JSON.stringify(obj))

但是该方法具有以下局限性:

  • Date 对象,会将其转化成字符串
  • 如果对象存在循环引用,会报错
  • 存在 Set、Map、正则、Error 对象,该方法会将其转成空对象字面量 { } ,如果存在 undefined,该方法会直接忽略
  • 不能序列化函数
structuredClone

这是个原生的新api,babel有对应的polyfill(core-js@3),可以放心用

对应的局限性:

  • Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
  • 克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
  • 对象的某些特定参数也不会被复制(getter、setter、原形链上的属性也不会被追踪以及复制)

详情参考:结构化克隆算法 - Web API 接口参考 | MDN

可转移对象
  • 简单定义:一些特定的对象可以进行“转移”,转移后原对象将被清空。obj1 -> obj2 后 obj2 会具有 obj1 的内容,而 obj1 将被掏空。
  • 详细定义:可转移对象 - Web API 接口参考 | MDN

可以利用 structuredClone 进行“转移”

这东西有什么具体用途吗?这个涉及 worker 的使用

使得 postMessage() API 能够接受不仅是字符串的消息,还接受 File、Blob、ArrayBuffer 和 JSON 对象等复杂类型的消息

lodash.cloneDeep

跟前面二者相比,它更强大,原型链可以继承,getter、setter可以拿下,函数可以被复制...

但是 Lodash 的 tree-shaking 可能和我们的使用心智有一些出入,如果项目脚手架没有处理该问题,引入方式也没有注意,那么会带来一些性能损耗。

解决方案可以参考:www.cnblogs.com/fancyLee/p/…

对比

原型链表现

class Foo {
  constructor(name) {
    this.name = name;
  }
  foo() {
    console.log("hello", this.name);
  }
}

const obj = new Foo("obj");
obj.__proto__.b = {};

const obj2 = lodash.cloneDeep(obj);
const obj3 = structuredClone(obj);

obj2.name = "lodash";
obj3.name = "structuredClone";

console.log(obj2, obj3); // obj2 的原型指向 Foo,obj3 直接指向 Object
console.log(obj.__proto__ === obj2.__proto__); // true
console.log(obj2.__proto__ === obj3.__proto__); // false

手写

丐版

简单做一个递归

const deepClone = (target) => {
  if (typeof target !== "object") return target;// 基本数据类型直接返回
  
  const res = Array.isArray(target) ? [] : {};	// 数组和对象的体现形式不同,要区分
  
  for (const key in target) {
    res[key] = deepClone(target[key]);
  }
  
  return res;
}

但是另外需要关注几个问题

  1. 循环引用引起的调用栈溢出
  2. 递归引起的调用栈溢出
  3. 复杂数据类型

循环引用

如果对象中有个属性是指向对象本身的,即 target.target = target

那么在深克隆的时候,一旦检测到 target.target 就会复制一个 target ,而这个被复制的对象,内部也需要不断复制,进而陷入死循环然后爆栈。

为了解决循环引用问题,我们可以额外开辟一个存储空间,用来存储已经被复制过的对象地址(基本数据类型直接被返回了),当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

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

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为 key,克隆对象作为 value 进行存储
  • 继续克隆
const deepClone = (target, map = new WeakMap()) => {
  if (typeof target !== "object") return target;// 基本数据类型直接返回
  
  if (map.has(target)) return map.get(target);	// 解决循环引用问题
  
  const res = Array.isArray(target) ? [] : {};	// 数组和对象的体现形式不同,要区分
  map.set(target, res);
  
  for (const key in target) {
    res[key] = deepClone(target[key], map); // 记得传入已有的 map
  }
  return res;
}

递归转遍历

递归层数够深的话,同样也会引起栈溢出的情况

...
const objGenerator = (obj = {}, depth = 6000) => {
  let prevObj = obj;

  do {
    const parentNode = prevObj[`${depth + 1}`] ?? obj;
    parentNode[`${depth}`] = {};
    prevObj = parentNode;
  } while (depth--);

  return obj;
};

const obj = objGenerator();

console.log(deepClone(obj)); // 爆了 4300
// console.log(lodash.cloneDeep(obj)); // 也爆了 4200(粗略看了眼源码,内部也是递归的方式)
// console.log(structuredClone(obj)); // 都爆了 1100

解决方案是用遍历的方式

function deepCloneV2(x) {
  const uniqueList = []; // 处理循环引用

  let root = {};

  // 循环数组
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    },
  ];

  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const { parent, key, data } = node;
    // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== "undefined") {
      res = parent[key] = Array.isArray(data) ? [] : {};
    }

    // 数据已经存在(处理循环引用)
    let uniqueData = uniqueList.find((item) => item.source === data);
    if (uniqueData) {
      parent[key] = uniqueData.target;
      continue; // 中断本次循环
    }

    // 数据未出现过
    uniqueList.push({
      source: data,
      target: res,
    });

    for (const key in data) {
      if (data.hasOwnProperty(key)) {
        const value = data[key];
        if (typeof value === "object") {
          loopList.push({
            parent: res,
            key,
            data: value,
          });
          // 其他判断可以挂 else...if...
        } else {
          // 不是对象不用进遍历队列
          res[key] = value;
        }
      }
    }
  }

  return root;
}
复杂数据类型

edge case特别多,不想写了