1. 主页 > 小妙招

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)

看见没?这就形成了:

  1. ??五层嵌套对象??(爷爷→爸爸→妈妈→hobby数组→元素)
  2. ??循环引用??(最内层反过来引用爷爷)

普通深拷贝方法遇到这种结构,要么复制不全,要么直接死循环卡死!


三、JSON大法好?先看看这些惨案现场

先说最常用的JSON.parse(JSON.stringify())方案:

javascript复制
const tryClone = obj => JSON.parse(JSON.stringify(obj))

??测试结果表??:

数据类型结果翻车指数
普通对象? 完美复制
Date对象? 变成字符串☆☆☆☆
函数属性? 直接消失☆☆☆☆☆
循环引用? 直接报错☆☆☆☆☆
Symbol类型? 直接消失☆☆☆☆

(敲黑板)去年我有个项目用这个方案处理订单数据,结果所有下单时间都变成字符串,直接导致日统计功能崩盘,血淋淋的教训啊!


四、手撕递归深拷贝的正确姿势

来上硬核方案!先记住这几个关键点:

  1. ??分诊处理不同数据类型??(日期、正则等特殊对象)
  2. ??准备记忆地图??(解决循环引用)
  3. ??递归到底的耐心??(处理无限嵌套)

上代码!注意看注释:

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
}

??这段代码的三大绝活??:

  1. WeakMap当"记仇本"解决循环引用
  2. 递归遍历所有层级的属性
  3. 单独处理特殊对象类型

五、循环引用的破解之道(说人话版)

举个真实场景:微信聊天记录导出

javascript复制
const msg1 = { text: "在吗" }
const msg2 = { replyTo: msg1 }
msg1.reply = msg2  // 形成环形引用

这时候如果用普通深拷贝,就像两个人互相指着对方说"你复制他"→"不,你复制他",直接陷入死循环!

??解决方案三板斧??:

  1. ??备忘录模式??:每次拷贝前先查账本(WeakMap)
  2. ??提前登记??:刚创建空对象时就记下对应关系
  3. ??递归时传递账本??:保证所有递归调用共享同一个账本

说人话就是:抄家的时候先把所有房间编号,看到标记过的房间直接贴个"已抄"封条,不用重复进去。


六、lodash的深拷贝真的万能吗?

先说结论:??能用第三方库就别自己造轮子??!拿lodash.clonedeep举例:

javascript复制
import _ from 'lodash'
const cloned = _.cloneDeep(original)

??实测对比表??:

对比项自写递归lodashJSON方案
嵌套对象???
循环引用???
函数属性???
Date对象???
Map/Set???
性能(万次/秒)120035005800

(拍桌子)看见没?自己写的递归在功能性和性能上都被吊打!但别急着哭,这恰恰说明:??理解原理是为了更好地使用工具??。


七、个人踩坑血泪史

刚学JS那会儿,我总觉得用库的都是菜鸡。直到有次在项目里手写深拷贝,遇到个包含20层嵌套+5处循环引用的数据结构,递归直接爆栈导致页面白屏。最后换成lodash节省了3天调试时间,从此悟了:

  • ??业务开发要效率?? → 无脑用成熟库
  • ??面试学习要原理?? → 必须会手写
  • ??简单场景要快捷?? → JSON方案凑合

最近发现Chrome 98+支持了原生structuredClone()方法,实测能处理循环引用,但Safari兼容性还是硬伤。这事儿告诉我们:前端永远在进化,但万变不离其宗。


八、送你一份避坑指南(记得收藏)

  1. 遇到undefinedsymbol作为属性值,很多深拷贝方案会直接丢弃
  2. 原型链上的属性不会被拷贝(所以要用hasOwnProperty过滤)
  3. 对象方法(函数属性)在JSON方案中会丢失,但业务中可能需要保留
  4. 最新的structuredClone()API已经支持DOM节点拷贝(比如复制canvas元素)
  5. 深拷贝性能消耗极大,超过10MB的数据建议改用不可变数据结构

最后说句掏心窝子的话:深拷贝就像谈恋爱,越想完全掌控对方的一切,就越可能陷入无限循环的困境。有时候,学会适当地"放手"(用不可变数据)反而能让代码关系更健康!

本文由嘻道妙招独家原创,未经允许,严禁转载