你写的深拷贝函数不会还不支持循环引用吧!

591 阅读6分钟

浅拷贝与深拷贝

前面先总结一下常见的深浅拷贝的方法,如需看手写深拷贝函数请直接到文章最后

浅拷贝

浅拷贝是指创建一个新对象,其中复制的是原对象的第一层属性值。对于原对象的基本数据类型,浅拷贝会直接复制它们的值;而对于引用类型,浅拷贝只复制引用,即新对象的属性和原对象的属性指向同一个内存地址。这样,如果修改引用类型的内部属性,原对象和拷贝对象都会受到影响。

(1)Object.assign()

  • Object.assign()  静态方法将一个或者多个源对象中所有可枚举自有属性复制到目标对象,并返回修改后的目标对象。

  • Object.assign() 实际上对每个源对象执行的是浅复制

const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 0, b: { c: 0 } }

obj1.a = 1;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 0, b: { c: 0 } }

obj2.a = 2;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 2, b: { c: 0 } }

obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }

(2)扩展运算符

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

代码解析

let obj1 = {a:1,b:{c:1}};
let obj2 = {...obj1};

首先,obj1 是一个嵌套对象,包含一个普通属性 a 和一个嵌套对象 b。通过扩展运算符,我们将 obj1 的第一层属性浅拷贝到了 obj2 中。这意味着 obj2 是一个新对象,但它的属性值与 obj1 相同。

第一次修改

obj1.a = 2;

此时,我们修改了 obj1.a 的值为 2。由于 a 是一个原始类型的值(数字),它的拷贝是独立的,因此修改 obj1.a 不会影响到 obj2.a

  • console.log(obj1); 输出 {a:2, b:{c:1}}
  • console.log(obj2); 输出 {a:1, b:{c:1}}

可以看到,obj1.a 的修改并没有影响 obj2.a

第二次修改

obj1.b.c = 2;

接下来,我们修改了嵌套对象 b 内的属性 c。由于扩展运算符进行的是浅拷贝,所以 obj1.bobj2.b 指向的是同一个引用。当我们修改 obj1.b.c 时,obj2.b.c 也会受到影响。

  • console.log(obj1); 输出 {a:2, b:{c:2}}
  • console.log(obj2); 输出 {a:1, b:{c:2}}

此时可以看到,尽管 obj2.a 没有变化,但 obj2.b.c 已经同步变化了,因为它与 obj1.b 指向同一个对象。

(3)数组方法实现数组浅拷贝

1)Array.prototype.slice
let arr = [1, { a: 1 }];
let copyArr = arr.slice();
copyArr[0] = 2;
copyArr[1].a = 2;
console.log(arr);
console.log(copyArr);
// [ 1, { a: 2 } ]
// [ 2, { a: 2 } ]

在这个例子中,copyArr[0] 是一个原始值,修改不会影响 arr,但 copyArr[1] 是一个对象的引用,因此修改 copyArr[1].a 也会影响到原数组 arr 的同一个对象。

2)Array.prototype.concat
let arr = [1, { a: 1 }];
let copyArr1 = arr.concat();

slice() 方法一样,concat() 也会对数组中的引用类型元素执行浅拷贝。

(4)手写实现浅拷贝

// 可能是数组的浅拷贝或对象的浅拷贝
function copyShallow(obj) {
  // 首先判断传入类型
  let newObj = Array.isArray(obj) ? [] : {};
  for(let key in obj) {
    if(obj.hasOwnProporty(key)) {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

深拷贝

深拷贝则是递归地复制对象的所有层级,包括嵌套的引用类型。深拷贝创建的是一个完全独立的新对象,原对象与拷贝对象之间没有共享的内存区域。因此,修改深拷贝对象中的任何属性,都不会影响原对象。

先给出拷贝的原始对象:

const original = {
  name: "MDN",
  money: 123n,
  b: Symbol('b'),
  c: null,
  d: undefined,
  e() {
    console.log(1);
  }
};
original.itself = original;

(1)JSON.stringify()

利用 JSON.stringify() 将对象转换为 JSON 字符串,再用 JSON.parse() 将 JSON 字符串解析为新的对象。

const clone1 = JSON.parse(JSON.stringify(original));
console.log(clone1);

这种方法简单易行,但有以下几个限制:

  • 无法拷贝BigInt(报错)
  • 无法处理对象的循环引用(报错)
  • 无法拷贝Symbol、function、undefined

(2)函数库lodash的_.cloneDeep方法

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false
</script>

(3)structuredClone(官方深拷贝API)

const original = { name: "MDN" };
original.itself = original;

// Clone it
const clone = structuredClone(original);

console.log(clone !== original);// true
console.log(clone.name === "MDN"); // true
console.log(clone.itself === clone); // true
console.log(clone);
  • 可以处理循环引用
  • 不能拷贝 Symbolfunction

(4)手写实现深拷贝

function deepCopy(obj) {
  const result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (obj[key] instanceof Object) {
        result[key] = deepCopy(obj[key]);
      } else {
        result[key] = obj[key];
      }
    }
  }
  return result;
}
const clone3 = deepCopy(original);
console.log(clone3);

image.png

限制

  1. 无法处理循环引用(栈溢出错误);
  2. 不能拷贝函数;

对象中存在循环引用,如果我们一味的递归其所有层级,会导致栈溢出错误。这里处理循环引用,我们需要使用到ES6新增的弱引用数据结构 WeakMap

代码实际并不复杂,难点是要考虑清楚如何借助弱引用的 WeakMap 处理循环引用,避免递归导致的溢出。现在我们就借助weapmap实现一个高级的深拷贝函数。

高级深拷贝函数

function deepCopy(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (hash.has(obj)) {
    return hash.get(obj);
  }

  const result = Array.isArray(obj) ? [] : {};
  hash.set(obj, result);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      console.log(obj[key] === original);
      result[key] = deepCopy(obj[key], hash);
    }
  }
  return result;
}

image.png

核心逻辑

  1. 检查是否是对象:函数首先判断是否是对象或 null,如果不是,直接返回(处理基本类型)。
  2. 利用 WeakMap 记录已拷贝的对象:每当处理一个对象时,先检查 WeakMap 中是否已存在该对象。如果存在,说明遇到了循环引用,直接返回之前记录的拷贝。
  3. 递归拷贝对象的属性:如果没有循环引用,继续递归拷贝对象的每个属性,并在 WeakMap 中记录当前对象的拷贝,防止后续递归中再次处理到它。

值得一提的是,我们的深拷贝函数中对于函数的拷贝在核心逻辑(1)中被处理,直接返回了原函数,也就是说,拷贝后的函数是原函数的引用,输出验证一下:

console.log(original.e === clone3.e);// true

最后

面试遇到深拷贝的题目,大家可以回答一下官方的API:structuredClone,面试官可能觉得你了解比较全面,不只是老一套的方法;也可以谈谈实现深拷贝函数时需要考虑的情况,比如递归拷贝,弱引用等等。

如果有更好的深拷贝实现方案,欢迎大家在评论区留言;文章中有错误的内容也可以直接在评论区指出!

如果觉得文章有所帮助还请点赞、评论、收藏。