算法 - 深拷贝(支持循环引用)
1. 题目
实现一个 deepClone(source),对一个值进行深拷贝,要求:
- 对象、数组递归拷贝,新旧互不影响(改新不影响旧)。
- 正确处理循环引用(
a.self = a),不能栈溢出。 - 尽量还原特殊类型:
Date、RegExp、Map、Set,以及Symbol作为 key。 - 函数、原始值直接返回(浅拷贝即可)。
示例:
1 | const obj = { name: "jack", tags: ["ts", "algo"], born: new Date() }; |
2. 解题思路
深拷贝 = 递归遍历 + 类型分发。两大难点:
2.1 循环引用:WeakMap 记忆化
若对象 A 引用了 B,B 又引用回 A,朴素递归会无限下钻爆栈。解决:用一个 WeakMap<源对象, 克隆对象> 记录「已经克隆过的源对象 -> 其克隆结果」。每次要克隆一个对象前,先查 WeakMap,命中就直接返回缓存的克隆体,从而打断回环并保持引用一致性。用 WeakMap 而非 Map 是为了不阻止源对象被 GC。
2.2 类型分发
原始值(
typeof !== "object"且非函数)→ 直接返回。null→ 直接返回。Date→new Date(+value)。RegExp→new RegExp(source, flags)。Map/Set→ 新建并递归克隆每个值(键一般也克隆以保险)。数组 →
map递归。普通对象 → 遍历自有属性(含
Symbolkey)递归。时间复杂度:
O(n),n 为节点数。空间复杂度:
O(n)(递归栈 + WeakMap)。
3. TypeScript 实现
1 | function deepClone<T>(source: T, cache = new WeakMap<object, any>()): T { |
划重点:
cache.set必须在递归子属性之前执行。若等子树克隆完再写缓存,遇到obj.self = obj时缓存里还没有 obj 的克隆体,就会无限递归。
4. 面试延伸
JSON.parse(JSON.stringify(obj))的坑:会丢失function、Symbol、undefined,把Date变字符串、RegExp变{},遇循环引用直接抛错——是必答的反面教材。structuredClone:现代浏览器/Node 内置的深拷贝,支持循环引用与多种内建类型,但不克隆函数、DOM 节点、原型链方法。能提到它说明关注标准 API。- 进阶要求:拷贝不可枚举属性 / getter / 原型链(本题用
Reflect.ownKeys+getPrototypeOf已部分覆盖);Symbol 作 key 的还原。 - 讲题结构建议:先点出「循环引用 + 特殊类型」两大难点,再用 WeakMap 破第一个,体现你先识别问题再逐个击破的思路。
难度:困难 | 前端手写题压轴 | 引用类型与递归