如何用JavaScript实现深拷贝?

来源:站长素材作者:小师妹头衔:草根站长
导读:本期聚焦于小伙伴创作的《如何用JavaScript实现深拷贝?》,敬请观看详情。对象赋值只传递引用导致修改副本影响原数据,这是前端最常踩的坑。深拷贝要求完全复制值并在堆中新建对象,基础类型直接返回,引用类型需递归遍历。原生JSON方法无法处理函数、循环引用和特殊对象,手写递归配合类型判断才能覆盖多数场景,同时可用WeakMap解决循环引用。理解这些差异能帮你避开数据污染的隐蔽故障。

在JavaScript开发中,我们经常需要复制一个对象或数组,但简单的赋值操作只是把引用地址传给了新变量。当原对象包含嵌套结构时,任何对副本的修改都会同步反映到原数据上。深拷贝的核心目标,是为每一层引用类型都创建全新的内存空间,使新旧数据彻底脱离关联。

如何用JavaScript实现深拷贝?

为什么需要深拷贝

JavaScript中的数据类型分为基本类型(如number、string、boolean)和引用类型(如object、array、function)。基本类型在赋值时会直接复制值,而引用类型赋值只是复制了指向堆内存的指针。如果项目中多个模块共享同一个配置对象,其中一个模块修改了嵌套属性,其他模块读取到的数据就会意外变化,引发难以排查的bug。

浅拷贝工具例如Object.assign或展开运算符,只能复制第一层的属性。对于嵌套的对象,它们依然保留原来的引用。因此在处理来自接口的动态数据、表单状态快照、或者撤销重做功能的历史记录时,必须借助深拷贝来保证数据独立性。

使用JSON方法的局限

最容易被想到的深拷贝方案是借助JSON.stringify和JSON.parse。这种方法写法简单,在很多纯数据场景下确实可用。但它存在明显的盲区:函数、undefined、Symbol会被直接丢弃;Date对象会被转为字符串而非Date实例;RegExp、Map、Set等内置对象会丢失原型和方法;最致命的是,如果对象存在循环引用,JSON.stringify会直接抛出错误。

下面这段代码演示了JSON方案在处理特殊值时的表现:

const source = {
  name: 'test',
  fn: function() { return 1; }, // 函数会被忽略
  time: new Date(),             // 变成字符串
  circle: null
};
source.circle = source;          // 循环引用

try {
  const copy = JSON.parse(JSON.stringify(source));
  console.log(copy.fn);   // undefined
  console.log(copy.time); // 字符串,不是Date对象
} catch (e) {
  console.log('报错:循环引用无法序列化');
}

从输出可以看出,JSON方案只适合处理“纯JSON数据”,也就是属性全是字符串、数字、布尔、数组和普通对象的场景。一旦数据结构复杂,就必须使用更完善的实现。

手写递归深拷贝函数

要覆盖更通用的场景,可以自己写一个递归函数。核心思路是:遇到基本类型直接返回;遇到引用类型,根据其构造函数类型创建对应的空容器,然后遍历键名递归拷贝每一项。为了处理循环引用,可以使用WeakMap记录已经拷贝过的对象,当再次遇到同一引用时直接返回缓存的副本。

下面的实现兼顾了数组、普通对象、Date和RegExp,并且通过WeakMap避免了循环引用导致的栈溢出:

function deepClone(target, cache = new WeakMap()) {
  // 基本类型或函数直接返回
  if (target === null || typeof target !== 'object') {
    return target;
  }
  // 处理循环引用
  if (cache.has(target)) {
    return cache.get(target);
  }
  // 处理Date和RegExp
  if (target instanceof Date) {
    return new Date(target);
  }
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags);
  }
  // 创建对应类型的容器
  const clone = Array.isArray(target) ? [] : {};
  cache.set(target, clone);
  // 递归拷贝可枚举自有属性
  Reflect.ownKeys(target).forEach(function(key) {
    clone[key] = deepClone(target[key], cache);
  });
  return clone;
}

// 测试
const obj = { a: 1, b: { c: 2 }, d: new Date() };
obj.self = obj;
const result = deepClone(obj);
console.log(result.b === obj.b); // false,已实现深拷贝
console.log(result.self === result); // true,循环引用被正确处理

这个函数在大多数业务代码中已经足够健壮。它使用Reflect.ownKeys拿到包括Symbol在内的所有键,保证特殊键名也不会遗漏。WeakMap的键是弱引用,不会造成内存泄漏,适合在拷贝过程中做临时缓存。

其他注意事项与优化

上述手写方案仍然没有处理Map、Set、ArrayBuffer等更多内置类型,也没有拷贝对象原型链上的方法。如果你面对的是极其复杂的数据结构,可以考虑使用社区成熟库如lodash的cloneDeep,它内部对几十种类型做了分支判断。另外,如果数据层级非常深且体积巨大,递归可能导致调用栈过长,此时可改为循环加栈结构的非递归写法。

在性能方面,深拷贝本身是一项开销较大的操作。实际开发中应避免在渲染循环里频繁深拷贝大对象,可以通过结构共享(如Immutable.js的思路)或只拷贝发生变化的部分来降低代价。理解深拷贝的原理和边界,才能在不同场景下选择最合适的复制策略。

JavaScript深拷贝递归修改时间:2026-08-07 05:36:22

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。