js深拷贝递归实现方法
手写JS深拷贝:告别JSON.stringify,递归到底该怎么写?
项目里经常遇到对象嵌套修改后,原数据悄悄跟着变的情况。浅拷贝只复制了一层引用,深层结构就像多米诺骨牌,一碰全倒。很多人第一反应是 JSON.parse(JSON.stringify(obj)),但遇到正则、函数或循环引用,它直接罢工。想要稳稳当当地克隆一份独立副本,递归确实是绕不开的底层逻辑。
递归之所以适合做深拷贝,是因为JavaScript的对象和数组本质上是树状结构。一层层往下剥,碰到叶子节点就停住并复制,这正是递归的拿手好戏。实战中写深拷贝,别急着铺代码,先把边界条件理顺。
精准拦截特殊类型。不能所有东西都往里塞,null、基本数据类型(字符串、数字、布尔等)、函数和Symbol必须直接返回原值。这部分判断放最前面,能避免后续逻辑跑偏,也能防止非对象数据触发多余的类型检查。
区分容器类型。普通的Object和Array走常规遍历;Date和RegExp需要保留原生实例,不能简单用空对象覆盖;Map和Set得用各自的原生构造函数重建。这里容易踩坑的是原型链丢失,深拷贝默认只复制数据属性,如果业务强依赖类的方法继承,需要额外处理,但大多数数据传递场景下,保留键值对就够了。
核心递归与环引用防御。纯递归最怕死循环,比如 obj.a = obj。引入 WeakMap 缓存已处理的对象是行业标准做法。遍历到子项时先查缓存,命中就直接返回,没命中才创建新实例并继续递归。这样既防住了栈溢出,又利用了弱引用特性,让垃圾回收器能够正常接管未引用的副本。
function deepClone(target, cache = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
if (cache.has(target)) return cache.get(target);
let cloneTarget = Array.isArray(target) ? [] : {};
cache.set(target, cloneTarget);
if (target instanceof Map) {
target.forEach((val, key) => cloneTarget.set(key, typeof val === 'object' ? deepClone(val, cache) : val));
} else if (target instanceof Set) {
target.forEach(val => cloneTarget.add(typeof val === 'object' ? deepClone(val, cache) : val));
} else {
for (let key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
cloneTarget[key] = typeof target[key] === 'object' ? deepClone(target[key], cache) : target[key];
}
}
const symbols = Object.getOwnPropertySymbols(target);
symbols.forEach(sym => {
cloneTarget[sym] = typeof target[sym] === 'object' ? deepClone(target[sym], cache) : target[sym];
});
}
return cloneTarget;
}
这段代码把常见类型兜底了,但实际落地时要注意两个细节。一是调用栈上限,递归处理万级嵌套的大对象极易触发 Maximum call stack size exceeded,面对复杂表单或配置树,建议改用队列迭代或分块处理。二是性能损耗,深拷贝全程新建对象和缓存查询,频繁调用会拖累渲染帧率,拷贝完记得及时释放冗余引用,别给内存监控留隐患。
如今浏览器已经内置 structuredClone(),处理纯数据流转时完全可以优先使用原生方案。但在Node服务端、老旧项目维护,或是需要自定义拷贝策略(比如过滤敏感字段、重组数据结构)的场景,手写递归依然能提供最高的可控性。把它封装进工具库,关键时刻能省下排查“隐式数据污染”的时间。
理解递归深拷贝的底层脉络,不是为了天天重复造轮子,而是为了在调试黑盒报错时心里有底。把类型守卫写扎实,把缓存机制配齐全,这份代码就能稳稳跑在生产环境。下次再遇到对象关联导致的诡异Bug,不妨静下心来跟一遍数据流向,真相往往藏在每一层递推的返回值里。


还没有评论,来说两句吧...