JS深拷贝如何正确处理嵌套对象?附循环引用解决方案
一、你的数据为什么总被同事"误伤"?
(拍大腿)这事儿我可太有发言权了!上周隔壁工位老王找我帮忙调试代码,我随手把自己写的配置对象发给他:
javascript复制const config = { api: "/user/login", params: { timestamp: new Date() }, childrenConfig: [{ id: 1 }] }
结果老王在子配置里加了个字段,我的原始数据突然多出个childrenConfig: [{id:1, debug:true}]
!你们猜怎么着???这就是浅拷贝在作怪??,所有嵌套对象都在共享内存地址,跟共用毛巾一样危险!
二、深拷贝的"俄罗斯套娃"难题
先来拆解这个经典死亡结构:
javascript复制const familyTree = { name: "爷爷", children: [{ name: "爸爸", spouse: { name: "妈妈", hobby: ["逛街", "追剧"] } }] } // 最狠的是最后加这句 ↓ familyTree.children[0].spouse.hobby.push(familyTree)
看见没?这就形成了:
- ??五层嵌套对象??(爷爷→爸爸→妈妈→hobby数组→元素)
- ??循环引用??(最内层反过来引用爷爷)
普通深拷贝方法遇到这种结构,要么复制不全,要么直接死循环卡死!
三、JSON大法好?先看看这些惨案现场
先说最常用的JSON.parse(JSON.stringify())
方案:
javascript复制const tryClone = obj => JSON.parse(JSON.stringify(obj))
??测试结果表??:
数据类型 | 结果 | 翻车指数 |
---|---|---|
普通对象 | ? 完美复制 | ☆ |
Date对象 | ? 变成字符串 | ☆☆☆☆ |
函数属性 | ? 直接消失 | ☆☆☆☆☆ |
循环引用 | ? 直接报错 | ☆☆☆☆☆ |
Symbol类型 | ? 直接消失 | ☆☆☆☆ |
(敲黑板)去年我有个项目用这个方案处理订单数据,结果所有下单时间都变成字符串,直接导致日统计功能崩盘,血淋淋的教训啊!
四、手撕递归深拷贝的正确姿势
来上硬核方案!先记住这几个关键点:
- ??分诊处理不同数据类型??(日期、正则等特殊对象)
- ??准备记忆地图??(解决循环引用)
- ??递归到底的耐心??(处理无限嵌套)
上代码!注意看注释:
javascript复制function deepClone(target, map = new WeakMap()) { // 如果是基本类型,直接回家吃饭 if(typeof target !== 'object' || target === null) return target // 检查是不是已经拷贝过这个对象 if(map.has(target)) return map.get(target) // 处理特殊对象 if(target instanceof Date) return new Date(target) if(target instanceof RegExp) return new RegExp(target) // 新建空对象&记录映射关系 const cloneObj = Array.isArray(target) ? [] : {} map.set(target, cloneObj) // 递归大冒险开始 for(let key in target) { if(target.hasOwnProperty(key)) { cloneObj[key] = deepClone(target[key], map) } } return cloneObj }
??这段代码的三大绝活??:
- WeakMap当"记仇本"解决循环引用
- 递归遍历所有层级的属性
- 单独处理特殊对象类型
五、循环引用的破解之道(说人话版)
举个真实场景:微信聊天记录导出
javascript复制const msg1 = { text: "在吗" } const msg2 = { replyTo: msg1 } msg1.reply = msg2 // 形成环形引用
这时候如果用普通深拷贝,就像两个人互相指着对方说"你复制他"→"不,你复制他",直接陷入死循环!
??解决方案三板斧??:
- ??备忘录模式??:每次拷贝前先查账本(WeakMap)
- ??提前登记??:刚创建空对象时就记下对应关系
- ??递归时传递账本??:保证所有递归调用共享同一个账本
说人话就是:抄家的时候先把所有房间编号,看到标记过的房间直接贴个"已抄"封条,不用重复进去。
六、lodash的深拷贝真的万能吗?
先说结论:??能用第三方库就别自己造轮子??!拿lodash.clonedeep举例:
javascript复制import _ from 'lodash' const cloned = _.cloneDeep(original)
??实测对比表??:
对比项 | 自写递归 | lodash | JSON方案 |
---|---|---|---|
嵌套对象 | ? | ? | ? |
循环引用 | ? | ? | ? |
函数属性 | ? | ? | ? |
Date对象 | ? | ? | ? |
Map/Set | ? | ? | ? |
性能(万次/秒) | 1200 | 3500 | 5800 |
(拍桌子)看见没?自己写的递归在功能性和性能上都被吊打!但别急着哭,这恰恰说明:??理解原理是为了更好地使用工具??。
七、个人踩坑血泪史
刚学JS那会儿,我总觉得用库的都是菜鸡。直到有次在项目里手写深拷贝,遇到个包含20层嵌套+5处循环引用的数据结构,递归直接爆栈导致页面白屏。最后换成lodash节省了3天调试时间,从此悟了:
- ??业务开发要效率?? → 无脑用成熟库
- ??面试学习要原理?? → 必须会手写
- ??简单场景要快捷?? → JSON方案凑合
最近发现Chrome 98+支持了原生structuredClone()
方法,实测能处理循环引用,但Safari兼容性还是硬伤。这事儿告诉我们:前端永远在进化,但万变不离其宗。
八、送你一份避坑指南(记得收藏)
- 遇到
undefined
或symbol
作为属性值,很多深拷贝方案会直接丢弃 - 原型链上的属性不会被拷贝(所以要用hasOwnProperty过滤)
- 对象方法(函数属性)在JSON方案中会丢失,但业务中可能需要保留
- 最新的
structuredClone()
API已经支持DOM节点拷贝(比如复制canvas元素) - 深拷贝性能消耗极大,超过10MB的数据建议改用不可变数据结构
最后说句掏心窝子的话:深拷贝就像谈恋爱,越想完全掌控对方的一切,就越可能陷入无限循环的困境。有时候,学会适当地"放手"(用不可变数据)反而能让代码关系更健康!
本文由嘻道妙招独家原创,未经允许,严禁转载