深拷贝

基础知识

在 JavaScript 中,对象的赋值默认是“浅拷贝”。理解深拷贝前,必须先区分它与浅拷贝的区别。

  1. 浅拷贝 (Shallow Clone): 只复制对象或数组的第一层属性。如果属性值是基本类型,就拷贝值;如果属性值是引用类型(如另一个对象或数组),则只拷贝其内存地址(引用)。因此,新旧对象共享同一个内部的引用类型数据。Object.assign() 和展开语法 {...obj} 都属于浅拷贝。

    const obj1 = { a: 1, b: { c: 2 } };
    const obj2 = { ...obj1 }; // 浅拷贝
    obj2.a = 100;
    obj2.b.c = 200;
    console.log(obj1); // { a: 1, b: { c: 200 } } -> obj1 的内部对象被修改了!
    
  2. 深拷贝 (Deep Clone): 完全创建一个新的、独立的副本。它会递归地复制一个对象及其所有子对象,所以新旧对象之间没有任何共享的引用。修改新对象不会对原始对象产生任何影响。

  3. 一个简单但不完美的深拷贝: JSON.parse(JSON.stringify(obj)) 是最广为人知的深拷贝技巧。它简单易用,但有诸多限制,无法处理复杂的场景:

    • 会忽略 undefinedSymbol 类型的属性和函数。
    • 无法处理 Date 对象(会转换成字符串)。
    • 无法处理 RegExpSetMap 等类型。
    • 最致命的是,无法处理循环引用(对象内部有属性指向自身),会直接抛出错误。

因此,要实现一个功能完备的深拷贝,我们需要手动编写一个更强大的函数。

核心思路

一个功能完善的深拷贝,其核心思想是递归,并在此基础上处理各种复杂情况。

  1. 递归遍历:

    • 定义一个函数,接收要拷贝的目标 target
    • 判断 target 的类型。如果是基本类型null,它们无需拷贝,直接返回自身。这是递归的出口
    • 如果是引用类型(对象、数组等),则创建一个新的空容器(新对象或新数组)。
    • 遍历 target 的所有属性,对每个属性值都递归调用深拷贝函数自身。
    • 将递归返回的结果赋值给新容器的对应属性。
    • 最终返回这个填充完毕的新容器。
  2. 处理特殊情况: 在上述递归的基础上,必须额外考虑并解决以下问题:

    • 循环引用: 如何防止因对象相互引用导致的无限递归?
    • 多种数据类型: 如何正确拷贝 Date, RegExp, Set, Map 等内置对象?
    • 属性全面性: 如何确保对象的 Symbol 属性或不可枚举属性也能被拷贝?

关键点 (处理复杂场景)

  1. 循环引用处理: 这是深拷贝中最关键的挑战。解决方案是使用一个缓存

    • 我们可以使用 MapWeakMap 作为缓存容器。在开始拷贝一个对象前,先检查缓存中是否已有该对象的记录。
    • 如果有,说明之前已经拷贝过,直接返回缓存中的副本,中断递归。
    • 如果没有,则为该对象创建一个新副本,并立即[原始对象, 新副本] 的映射关系存入缓存,然后再进行递归。
    • 推荐使用 WeakMap,因为它对键是弱引用,当原始对象在外部被垃圾回收时,WeakMap 中的对应条目会自动消失,能有效防止内存泄漏。
  2. 多种数据类型处理: 需要通过 instanceof 对不同的内置对象类型进行判断,并采用各自特定的方式进行克隆。例如,new Date(originalDate)

  3. 保持原型: 为了让克隆体的类型与源对象一致(如数组克隆后还是数组),我们可以通过 new target.constructor() 来创建新的容器,这可以继承原始对象的原型。

  4. 完整的属性遍历: 为了拷贝包括 Symbol 键在内的所有自身属性,应使用 Reflect.ownKeys(),它比 Object.keys()for...in 更为全面。

代码实现

下面的代码实现了一个考虑了上述所有关键点的 deepClone 函数。

/**
 * 实现一个功能全面的深拷贝函数
 *
 * @param {any} target - 需要被深拷贝的目标值
 * @param {WeakMap<object, any>} [cache=new WeakMap()] - 用于缓存对象,解决循环引用问题
 * @returns {any} - 返回深拷贝后的新值
 */
function deepClone(target, cache = new WeakMap()) {
  // 1. 对于基本类型和 null,直接返回
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理特殊引用类型:Date 和 RegExp
  if (target instanceof Date) {
    return new Date(target);
  }
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags);
  }

  // 3. 解决循环引用:如果缓存中已存在,则直接返回缓存的副本
  if (cache.has(target)) {
    return cache.get(target);
  }

  // 4. 初始化拷贝后的容器(对象、数组、Set、Map等)
  const copy = new target.constructor();

  // 5. 将新创建的拷贝放入缓存,防止后续循环引用 (这一步必须在递归之前)
  cache.set(target, copy);

  // 6. 处理 Map
  if (target instanceof Map) {
    target.forEach((value, key) => {
      copy.set(deepClone(key, cache), deepClone(value, cache));
    });
    return copy;
  }

  // 7. 处理 Set
  if (target instanceof Set) {
    target.forEach((value) => {
      copy.add(deepClone(value, cache));
    });
    return copy;
  }

  // 8. 遍历对象的自身属性(包括 Symbol 属性),并进行递归
  Reflect.ownKeys(target).forEach((key) => {
    copy[key] = deepClone(target[key], cache);
  });

  return copy;
}

代码解析

  1. 处理基本类型和 null:这是递归的出口。当 targetnull 或基本类型(如 numberstringboolean 等)时,直接返回自身,无需进一步拷贝。因为基本类型在 JavaScript 中是按值传递的,无需创建副本。
  2. 处理 Date 和 RegExp
    • targetDate 实例,通过 new Date(target) 创建新日期对象,确保时间值一致。
    • 若为 RegExp 实例,使用 new RegExp(target.source, target.flags) 复制正则表达式的模式和标志。
  3. 检查缓存(解决循环引用)
    • 利用 WeakMap 作为缓存容器,键为原始对象,值为对应的副本。
    • cache.has(target)true 时,说明当前对象已被拷贝过(存在循环引用),直接返回缓存中的副本以中断递归。
  4. 创建新容器:通过 new target.constructor() 生成与原始对象同类型的新容器(如数组、普通对象等),确保原型链正确继承(例如数组克隆后仍为数组)。
  5. 预置缓存(关键步骤):在递归处理对象属性前,先将新创建的空副本存入缓存 cache.set(target, copy)。这样即使对象内部属性引用自身,后续递归时也能通过缓存直接获取副本,避免无限递归。
  6. 处理 Map 类型
    • 遍历原始 Map 的键值对,对每个键和值分别进行深拷贝(deepClone(key, cache)deepClone(value, cache))。
    • 通过 copy.set() 将拷贝后的键值对存入新 Map
  7. 处理 Set 类型
    • 遍历原始 Set 的值,对每个值进行深拷贝(deepClone(value, cache))。
    • 通过 copy.add() 将拷贝后的值存入新 Set
  8. 遍历对象属性并递归拷贝
    • 使用 Reflect.ownKeys(target) 获取对象所有自身属性键(包括 Symbol 类型和不可枚举属性)。
    • 对每个属性值 target[key] 递归调用 deepClone,并将结果赋值给新对象 copy[key]
    • cache 参数透传确保整个拷贝过程共享同一缓存,避免重复拷贝和循环引用。
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""