手写深拷贝(Deep Clone):如何优雅处理 RegExp、Date 和循环引用(Circular Ref)?

手写深拷贝(Deep Clone):优雅处理 RegExp、Date 和循环引用(Circular Ref)

大家好,欢迎来到今天的编程技术讲座。今天我们要深入探讨一个看似简单但实则非常复杂的主题——手写深拷贝(Deep Clone)

你可能已经用过 JSON.parse(JSON.stringify(obj)) 来做深拷贝,但它有明显的局限性:无法处理 DateRegExp、函数、undefined、Symbol 等类型,更别说循环引用了。而我们今天的目标是写出一个真正健壮、优雅且能处理边界情况的深拷贝函数


一、为什么需要深拷贝?

在 JavaScript 中,对象和数组都是引用类型。如果你直接赋值:

const obj1 = { a: 1, b: [2, 3] };
const obj2 = obj1;
obj2.b.push(4);
console.log(obj1.b); // [2, 3, 4]

你会发现 obj1 也被改变了。这就是浅拷贝的问题。

深拷贝的核心目标是:创建一个新的对象或数组,其内部结构完全独立于原对象,修改新对象不会影响原对象。


二、常见深拷贝方法对比

方法 是否支持 Date 是否支持 RegExp 是否支持循环引用 性能 适用场景
JSON.parse(JSON.stringify(obj)) ⚡️快 简单数据结构(无特殊类型)
Lodash 的 _.cloneDeep() 🐢慢(功能强大) 生产环境推荐
自定义实现(本文重点) ⚡️中等 教学、理解原理、定制需求

💡结论:自定义深拷贝虽然复杂,但在理解底层机制和性能优化上有不可替代的价值。


三、核心挑战与解决方案设计

我们要解决三个关键问题:

1. 如何识别并正确复制特殊对象?

  • Date: 使用 new Date(date.getTime())
  • RegExp: 使用 new RegExp(regexp.source, regexp.flags)
  • Map / Set: 需要单独处理
  • Function: 通常不拷贝(除非特别要求)

2. 如何避免无限递归?——循环引用检测

当对象 A 引用了 B,B 又间接或直接引用了 A,就会形成环。如果不加限制,递归会死循环。

解决方案:

  • 使用 WeakMap 缓存已遍历的对象及其副本
  • 每次遇到一个对象时,先查缓存,存在就返回缓存值,不存在再递归

3. 如何保证类型一致性?

  • 数组 → 数组
  • 对象 → 对象
  • 原生对象(如 Date)→ 原生对象
  • 不能把 Array 当成普通 Object 处理!

四、完整代码实现(带详细注释)

下面是我们的核心深拷贝函数:

function deepClone(obj, visited = new WeakMap()) {
  // 1. 基本类型直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 如果是已有对象,则返回缓存中的副本(防止循环引用)
  if (visited.has(obj)) {
    return visited.get(obj);
  }

  let cloned;

  // 3. 特殊对象处理
  if (obj instanceof Date) {
    cloned = new Date(obj.getTime());
  } else if (obj instanceof RegExp) {
    cloned = new RegExp(obj.source, obj.flags);
  } else if (obj instanceof Map) {
    cloned = new Map();
    obj.forEach((value, key) => {
      cloned.set(key, deepClone(value, visited));
    });
  } else if (obj instanceof Set) {
    cloned = new Set();
    obj.forEach(value => {
      cloned.add(deepClone(value, visited));
    });
  } else if (Array.isArray(obj)) {
    // 4. 数组
    cloned = obj.map(item => deepClone(item, visited));
  } else {
    // 5. 普通对象
    cloned = {};
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        cloned[key] = deepClone(obj[key], visited);
      }
    }
  }

  // 6. 缓存当前对象及其副本(用于后续循环引用检测)
  visited.set(obj, cloned);

  return cloned;
}

五、逐层解析关键逻辑

✅ 第一步:基础类型判断

if (obj === null || typeof obj !== 'object') {
  return obj;
}

这是最基础的保护机制。如果传入的是数字、字符串、布尔值、null、undefined,直接返回即可。

✅ 第二步:循环引用检测(核心!)

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

这里使用了 WeakMap 而不是普通 Map,因为:

  • WeakMap 的键是弱引用,不会造成内存泄漏;
  • 它适合用来缓存对象映射关系;
  • 不会被垃圾回收器阻止(安全)。

✅ 第三步:特殊对象处理(优雅封装)

Date 类型

cloned = new Date(obj.getTime());

通过 .getTime() 获取时间戳再构造新 Date,是最稳妥的方式。

RegExp 类型

cloned = new RegExp(obj.source, obj.flags);

注意:source 是正则表达式的文本部分,flags 是修饰符(如 g, i, m),必须保留。

Map / Set 类型

这两个是 ES6 新增的数据结构,必须单独处理,否则会被当作普通对象处理(导致数据丢失)。

示例:

const map = new Map([['a', 1], ['b', 2]]);
const cloned = deepClone(map);
console.log(cloned.size); // 2

数组 vs 普通对象

cloned = obj.map(...); // 数组
// 或者
cloned = {}; // 普通对象

这确保了类型不变,比如 Array.isArray(obj) 必须保持为 true。


六、测试验证(含边界情况)

让我们用几个典型例子来测试我们的函数是否正确工作:

测试 1:基本对象 + 数组

const original = {
  name: "Alice",
  hobbies: ["reading", "coding"],
  age: 25
};
const cloned = deepClone(original);

cloned.hobbies.push("travel");
console.log(original.hobbies); // ["reading", "coding"]
console.log(cloned.hobbies);   // ["reading", "coding", "travel"]

✅ 正确分离!

测试 2:Date 和 RegExp

const obj = {
  date: new Date("2023-01-01"),
  regex: /hello/gi
};

const cloned = deepClone(obj);

cloned.date.setFullYear(2024);
cloned.regex.lastIndex = 10;

console.log(obj.date.getFullYear()); // 2023
console.log(cloned.date.getFullYear()); // 2024
console.log(obj.regex.lastIndex); // 0
console.log(cloned.regex.lastIndex); // 10

✅ 分离成功!

测试 3:循环引用(最关键!)

const a = { name: "A" };
const b = { name: "B", ref: a };
a.ref = b; // 形成环

const cloned = deepClone(a);

console.log(cloned.ref.ref.ref.name); // "A"
console.log(cloned.ref.ref === cloned); // false(不是同一个对象)

✅ 循环引用被正确处理,不会死循环!

测试 4:Map 和 Set

const map = new Map([['key', 'value']]);
const set = new Set([1, 2, 3]);

const clonedMap = deepClone(map);
const clonedSet = deepClone(set);

console.log(clonedMap.size); // 1
console.log(clonedSet.size); // 3

✅ 完美支持!


七、性能优化建议(进阶)

虽然上述实现已经很健壮,但在大规模数据下仍可进一步优化:

优化点 描述 实现方式
缓存策略 使用 WeakMap 已经很好,但可以考虑对某些高频类型预设缓存 Date.prototype.clone
类型判断 减少重复调用 instanceof 使用 Object.prototype.toString.call(obj) 更高效
递归深度限制 防止极端嵌套导致栈溢出 添加最大递归层数参数(如 maxDepth=100)

例如改进版的类型判断:

function getType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1);
}

// 使用:
if (getType(obj) === 'Date') { ... }

这样比多次 instanceof 更快,尤其在大量对象遍历时。


八、常见误区与陷阱提醒

误区 错误做法 正确做法
以为 for...in 就能遍历所有属性 忽略原型链上的属性 使用 Object.keys(obj)Reflect.ownKeys(obj)
忽略 Symbol 属性 默认跳过 Symbol 显式处理:Reflect.ownKeys(obj) 包含 Symbol
误以为 new Object(){} 一样 实际上两者行为一致 无需担心,统一处理即可
深拷贝函数不传 visited 参数 导致循环引用报错 必须传递缓存容器,首次调用时初始化为空

🔍 提醒:不要试图用 JSON.stringify 解决所有问题,它根本做不到!


九、总结:如何做到“优雅”的深拷贝?

我们最终达成的目标是:

功能完备:支持 Date、RegExp、Map、Set、数组、普通对象
安全性高:自动处理循环引用,避免栈溢出
性能合理:使用 WeakMap 缓存,减少冗余操作
易扩展:模块化设计,方便添加新的类型支持(如 TypedArray、Error 等)

这不是一次简单的编码练习,而是对 JS 内存模型、对象机制、递归控制和边界条件的全面考验。

正如一位资深工程师所说:“真正的深拷贝,不是写出来的,而是想明白后自然浮现的结果。”


十、附录:完整工具函数(可用于项目)

/**
 * 深拷贝函数(支持 Date、RegExp、Map、Set、循环引用)
 * @param {*} obj 待拷贝对象
 * @returns {*} 深拷贝后的对象
 */
function deepClone(obj, visited = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (visited.has(obj)) return visited.get(obj);

  let cloned;

  if (obj instanceof Date) {
    cloned = new Date(obj.getTime());
  } else if (obj instanceof RegExp) {
    cloned = new RegExp(obj.source, obj.flags);
  } else if (obj instanceof Map) {
    cloned = new Map();
    obj.forEach((value, key) => cloned.set(key, deepClone(value, visited)));
  } else if (obj instanceof Set) {
    cloned = new Set();
    obj.forEach(value => cloned.add(deepClone(value, visited)));
  } else if (Array.isArray(obj)) {
    cloned = obj.map(item => deepClone(item, visited));
  } else {
    cloned = {};
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        cloned[key] = deepClone(obj[key], visited);
      }
    }
  }

  visited.set(obj, cloned);
  return cloned;
}

你可以把这个函数直接放进你的工具库中,作为生产级使用的深拷贝方案。


希望今天的讲解让你不仅学会了怎么写深拷贝,更重要的是理解了背后的设计哲学:优雅 = 功能完整 + 安全可靠 + 易于维护。

下次你在面试或者工作中遇到这个问题时,不妨自信地说一句:“我写过一个能处理循环引用和特殊类型的深拷贝。” 😊

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注