「JS硬气功👊」不允许你只会一种深拷贝方式(一次回顾所有深拷贝方式)

473 阅读3分钟

Hi,这里是JustHappy,深拷贝是开发中常用的一个操作,但是方法不止一种,各有利弊,我们来整理整理吧

image.png

最简单的展开运算符

对于简单的只有单层的复杂数据类型,我们可以通过展开运算符来处理,这种方式只有一行代码

  • 优点

    • 代码简洁,易于理解。
    • 性能较好,适合简单对象的拷贝。
  • 缺点

    • 仅适用于对象的第一层属性,无法实现真正的深拷贝。如果对象的属性值是另一个对象或数组,展开运算符只会拷贝引用,不会递归拷贝内部结构。
    • 不能处理特殊数据类型(如 DateRegExpMapSet 等)。
  • 例子

// 拷贝对象1:单层的数据
const obj1 = {
  name: "JustHappy",
  age: 22,
  sex: "male",
};

/**
 * 对于单层的数据,可以直接使用展开运算符进行拷贝
 */

const copy1 = { ...obj1 };
console.log(copy1 === obj1); // false    深拷贝成功

当然,如果你使用展开运算符去拷贝多层嵌套的对象,打印出来你会发现也一样,但是从第二层开始的就不是深拷贝了。而是浅拷贝,如下

// 拷贝对象2:多层的数据
const obj1 = {
  name: "JustHappy",
  age: 22,
  sex: "male",
  friend: {
      name: "Tom"
  }
};

const copy1 = { ...obj1 };
console.log(copy1.friend === obj1.friend); // true    这里的friend是浅拷贝

JSON.stringify + JSON.parse

通过将对象序列化为 JSON 字符串,再反序列化为新的对象,从而实现深拷贝。这个方式相比展开运算符是可以可以处理嵌套对象的

  • 优点

    • 实现简单,代码量少。
    • 可以处理嵌套对象。
  • 缺点

    • 不能处理特殊数据类型(如 DateRegExpFunctionMapSet 等),这些类型会被忽略或转换为其他形式。
    • 无法拷贝对象的原型链。
    • 性能较差,尤其是对于复杂对象,因为涉及到字符串序列化和反序列化。
  • 例子

// 拷贝对象2:多层的数据
const obj1 = {
  name: "JustHappy",
  age: 18,
  sex: "male",
  friends: [
    {
      name: "Tom",
      age: 18,
      sex: "male",
    },
    {
      name: "Jerry",
      age: 18,
      sex: "male",
    },
  ],
};


const copy1 = JSON.parse(JSON.stringify(obj1));
console.log(copy1 === obj1); // false
console.log(copy1.friends === obj1.friends); // false 

递归深拷贝

通过递归遍历对象的每个属性,逐层拷贝其值。如果属性值是对象或数组,则递归调用拷贝函数

  • 优点

    • 以实现真正的深拷贝。
    • 可以自定义处理特殊数据类型(如 DateMapSet 等)。
  • 缺点

    • 代码较为复杂,需要手动处理各种数据类型。
    • 如果对象结构过深,可能会导致调用栈溢出。
  • 例子

// 拷贝对象2:多层的数据
const obj1 = {
  name: "JustHappy",
  age: 18,
  sex: "male",
  friends: [
    {
      name: "Tom",
      age: 18,
      sex: "male",
    },
    {
      name: "Jerry",
      age: 18,
      sex: "male",
    },
  ],
};


function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) return obj;
  let result;
  // 判断是数组还是对象还是其他的
  if (Array.isArray(obj)) {
    result = [];
  } else if (typeof obj === "object") {
    result = {};
  }
  for (let key in obj) {
    // 保证 key 不是原型上的属性:这样避免死循环
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }
  return result;
}


const copy2 = deepClone(obj1);
console.log(copy2 === obj1); // false
console.log(copy2.friends === obj1.friends); // false

如果你需要识别并深拷贝 DateMapSet 这些,只需要加些逻辑就好了。就像下面这样

function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) return obj;
  let result;
  if (Array.isArray(obj)) {
    result = [];
  } else if (typeof obj === "object") {
    result = {};
  }
  // 添加的逻辑
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof Map) return new [...Mapobj]();
  if (obj instanceof Set) return new Set([...obj]);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }
  return result;
}

一劳永逸的 structuredClone

对!你是不是烦了,有没有一种方法一劳永逸呢?不用思考那么多呢?

有的!structuredClone 是 ES2023 新增的内置方法,用于实现深拷贝。

  • 优点

    • 原生支持,性能较好。
    • 可以处理复杂数据类型(如 DateMapSetArrayBuffer 等)。
    • 自动处理循环引用。
  • 缺点

    • 浏览器兼容性有限(需要检查浏览器支持情况)。
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const newObj = structuredClone(obj);

console.log(newObj); // { a: 1, b: { c: 2 }, d: [3, 4] }
console.log(obj.b === newObj.b); // false(b 属性是深拷贝)