Map 能解决循环引用问题是因为它以对象引用为键缓存已拷贝对象,递归前查表命中则直接返回,避免重复遍历;其支持任意值作键且用===比较,精准识别对象身份。

JavaScript 中用 Map 记录对象引用,核心是**在递归拷贝过程中缓存已处理的对象及其拷贝结果**,避免重复遍历同一对象,从而打破循环引用导致的无限递归或栈溢出。
为什么 Map 能解决循环引用问题
深拷贝遇到循环引用(如 a.b = b; b.a = a)时,若不记录已拷贝的对象,递归会陷入死循环。使用 Map 作为“记忆表”,把原始对象作为 key、对应拷贝后的对象作为 value,每次进入拷贝前先查表——若存在,直接返回缓存结果,跳过递归。
Map 支持以任意值(包括对象、函数、Symbol)为键,且键比较基于引用相等(===),正好匹配对象身份识别需求;而普通对象或 WeakMap 在此处不如 Map 灵活(WeakMap 不能遍历,且键必须是对象,无法处理 null/undefined 等边界情况)。
手动实现带循环检测的深拷贝
以下是一个简洁可靠的实现,支持普通对象、数组、Date、RegExp、Map、Set,以及循环引用:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
立即学习“Java免费学习笔记(深入)”;
- 创建空
Map实例,用于存储original → clone映射 - 递归函数接收源值和缓存 Map,开头就检查是否已缓存,有则直接返回
- 对非基本类型(object、function、array 等)分配新实例,并立即存入 Map(防止自身嵌套时再次触发)
- 再逐个属性递归拷贝,确保子结构也走相同流程
示例代码:
function deepClone(obj, cache = new Map()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
let cloned;
const Ctor = obj.constructor;
if (obj instanceof Date) cloned = new Date(obj.getTime());
else if (obj instanceof RegExp) cloned = new RegExp(obj);
else if (obj instanceof Map) {
cloned = new Map();
cache.set(obj, cloned);
obj.forEach((val, key) => cloned.set(deepClone(key, cache), deepClone(val, cache)));
}
else if (obj instanceof Set) {
cloned = new Set();
cache.set(obj, cloned);
obj.forEach(val => cloned.add(deepClone(val, cache)));
}
else if (Array.isArray(obj)) {
cloned = [];
cache.set(obj, cloned);
obj.forEach((item, i) => cloned[i] = deepClone(item, cache));
}
else {
cloned = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, cloned);
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
cloned[key] = deepClone(obj[key], cache);
}
}
}
return cloned;
}
注意事项与边界处理
实际使用中需留意几个关键点:
- 缓存 Map 必须传入递归调用链:不能每次新建,否则缓存失效
-
构造函数兼容性:对自定义类实例,若需保留原型链,用
Object.create(prototype)比{}更准确;但方法不会被拷贝(深拷贝通常只处理数据) -
不可枚举属性、Symbol 键、getter/setter:上述示例未处理,如需可扩展
Object.getOwnPropertyDescriptors+Object.defineProperties - 函数和 undefined:默认不拷贝(函数是引用,undefined 不参与递归),按需决定是否浅复制或忽略
对比 JSON.parse(JSON.stringify()) 和 structuredClone
JSON.stringify() 会静默丢弃函数、undefined、Symbol、循环引用,直接报错;structuredClone()(现代浏览器支持)原生支持循环引用和多种内置类型,内部就用了类似 Map 的机制,但不支持函数、RegExp、Date 等部分类型(具体支持列表依环境而定)。手写方案的优势在于可控性强、可定制、兼容性好。

















