js深拷贝递归实现方法

2026-05-27 18:00:30 1925阅读 0评论

手写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,不妨静下心来跟一遍数据流向,真相往往藏在每一层递推的返回值里。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1925人围观)

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

目录[+]