JavaScript深拷贝实现指南:JSON方法、structuredClone与递归方案

JavaScript 深拷贝是前端开发里经常遇到的问题。对象和数组是引用类型,直接赋值只会复制引用,不会复制内部数据。修改新变量时,原对象也可能被影响,这在表单编辑、状态管理、缓存数据处理里很容易出问题。

深拷贝的目标,是创建一份新的数据结构,让新对象和原对象在嵌套层级上也互不影响。本文从浅拷贝和深拷贝区别讲起,再对比 JSON 方法、structuredClone 和递归实现方案。

浅拷贝问题

浅拷贝只复制第一层属性。如果属性值是对象或数组,复制过去的仍然是引用。

const user = {
  name: 'Alice',
  profile: { age: 20 }
};

const copy = { ...user };
copy.profile.age = 21;

console.log(user.profile.age);

上面代码里,profile 仍然指向同一个对象,所以修改副本的嵌套属性会影响原对象。

什么是深拷贝

深拷贝会递归复制对象内部的对象和数组,让每一层引用都变成新的。这样修改副本时,不会影响原始数据。

并不是所有场景都需要深拷贝。如果只是处理一层简单对象,浅拷贝就足够。深拷贝成本更高,也更容易遇到特殊类型和循环引用问题。

JavaScript深拷贝教程配图:对象复制与数据结构处理
深拷贝要解决的核心问题,是嵌套对象和数组共享同一引用导致的数据联动。

JSON 方法

最常见的简单深拷贝写法,是先把对象转成 JSON 字符串,再解析回来。

const copy = JSON.parse(JSON.stringify(obj));

这种方法简单粗暴,适合纯数据对象,比如只包含字符串、数字、布尔值、普通数组和普通对象的接口数据。

JSON 方法局限

JSON 方法有明显局限。它会丢失 undefined、函数、Symbol,无法正确处理 DateMapSet、正则等特殊类型,也无法处理循环引用。

const obj = {
  date: new Date(),
  fn: () => {},
  value: undefined
};

如果数据来自后端接口,并且结构很干净,JSON 方法可以临时使用;如果数据里有复杂类型,就不适合。

structuredClone

structuredClone 是现代浏览器和运行环境提供的原生深拷贝方法。它能处理更多类型,比如 DateMapSet、ArrayBuffer,也能处理循环引用。

const copy = structuredClone(obj);

相比 JSON 方法,structuredClone 更可靠,也更语义化。如果项目运行环境支持它,普通深拷贝优先考虑这个方法。

structuredClone 限制

structuredClone 也不是万能的。它不能克隆函数、DOM 节点等不可结构化克隆的值。如果对象里有函数,会抛出错误。

因此在使用前要明确数据类型。如果你要复制的是状态数据、接口数据、配置对象,它通常很好用;如果对象里混合了方法、节点和复杂实例,就要谨慎。

递归实现思路

手写深拷贝的基本思路是:如果是基本类型,直接返回;如果是数组,创建新数组递归复制每一项;如果是对象,创建新对象递归复制每个属性。

function deepClone(value) {
  if (value === null || typeof value !== 'object') {
    return value;
  }

  if (Array.isArray(value)) {
    return value.map((item) => deepClone(item));
  }

  const result = {};
  for (const key in value) {
    result[key] = deepClone(value[key]);
  }
  return result;
}

这个版本适合理解原理,但还不够完善。它没有处理循环引用,也没有处理 DateMapSet 等特殊类型。

处理循环引用

循环引用会让普通递归陷入死循环。可以使用 WeakMap 记录已经克隆过的对象。

function deepClone(value, cache = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value;

  if (cache.has(value)) return cache.get(value);

  const result = Array.isArray(value) ? [] : {};
  cache.set(value, result);

  for (const key in value) {
    result[key] = deepClone(value[key], cache);
  }

  return result;
}

WeakMap 的 key 可以是对象,适合记录原对象和克隆对象之间的映射,避免循环引用导致无限递归。

特殊类型处理

如果要写更完整的深拷贝,还要处理 Date、正则、MapSet 等类型。

if (value instanceof Date) {
  return new Date(value.getTime());
}

if (value instanceof RegExp) {
  return new RegExp(value.source, value.flags);
}

继续扩展当然可以,但代码会越来越复杂。实际项目里,如果没有特别需求,不建议自己维护一个过度复杂的深拷贝函数。

对象实例问题

类实例、带原型方法的对象,在深拷贝时也要格外注意。简单递归通常只复制普通属性,不一定保留原型链和方法行为。

如果数据本身是业务模型实例,最好不要随意深拷贝。更清晰的做法是提供明确的转换方法,或者只拷贝需要的纯数据部分。

性能注意事项

深拷贝会遍历整个数据结构,数据越大成本越高。在大型状态树、复杂列表、频繁交互中,盲目深拷贝可能带来性能问题。

如果只是更新对象的一小部分,可以考虑结构共享、局部浅拷贝,或者使用不可变数据更新工具。不要把深拷贝当成解决所有引用问题的万能方案。

怎么选择

如果是简单纯 JSON 数据,可以用 JSON 方法;如果运行环境支持,优先考虑 structuredClone;如果需要兼容旧环境或定制行为,可以写递归函数;如果对象类型复杂,建议使用成熟工具库或重新设计数据结构。

常见错误

第一种错误是以为展开运算符就是深拷贝。第二种错误是 JSON 方法用在包含 Date、函数或 undefined 的对象上。第三种错误是手写递归没有处理循环引用。第四种错误是在高频操作里反复深拷贝大对象。

实践建议

深拷贝前先判断是否真的需要。能用局部浅拷贝解决,就不要复制整棵数据树。确实需要深拷贝时,优先根据数据类型选择方案,而不是固定套某一种写法。

对大多数前端业务来说,深拷贝的重点不是背代码,而是理解引用关系、数据类型和性能成本。知道什么时候该拷贝、拷贝到什么程度,才是真正能避免 bug 的关键。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容