必须用 WeakSet 而非 Set,因其弱引用避免内存泄漏;判断需在添加前执行:先 seen.has(obj),为 true 则返回,否则再 seen.add(obj)。
用 WeakSet 防止深克隆中的循环引用死循环,核心是“在进入对象前检查是否已处理过”,而不是靠它自动跳过或拦截。WeakSet 本身不改变递归逻辑,只提供安全的、不泄漏内存的存在性标记。
为什么必须用 WeakSet 而不是 Set
Set 会强持有对象引用,导致本该被回收的原始对象一直滞留在内存中——尤其在长期运行的克隆任务(如状态树 diff、服务端序列化)里,容易引发内存堆积。WeakSet 的成员是弱引用:只要对象在别处没了强引用,GC 就能回收它,WeakSet 中对应条目也自动消失。这正好匹配深克隆场景中“只关心当前是否正在处理该对象”的临时需求。
关键操作顺序不能错
WeakSet 的判断和添加必须严格按以下顺序执行,否则无效:
每次递归入口处,先调用
seen.has(obj)
判断
如果返回 true,直接返回原对象(或抛错/返回占位符),不再继续递归
只有返回 false 时,才调用
seen.add(obj)
绝不能先 add 再 has ——刚加进去就进递归,等于没防住
实际克隆函数怎么写
一个典型实现如下,支持普通对象、数组,并兼容 null 和原始值:
注意:WeakSet 必须作为参数传入递归调用,不能依赖闭包捕获——否则不同克隆任务之间会互相干扰;也不建议声明为模块级常量,除非你明确要跨调用共享状态。
它不能解决哪些问题
WeakSet 只负责“标记已处理”,不负责:
自动展开或替换循环引用(比如替换成 $ref 字符串)
处理 JSON.stringify 这类黑盒方法——它们内部不暴露遍历钩子,WeakSet 插不进去
解除其他强引用链(如事件监听器、闭包变量、全局缓存),这些仍需手动清理,否则节点无法真正释放
它只是让递归过程安全、轻量、无内存负担地跑完。
function deepClone(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return obj; // 循环引用点,直接返回原引用
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item, seen));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key], seen);
}
}
return cloned;
}