JavaScript 深浅拷贝:JSON 陷阱、循环引用与四种正确姿势

举报
yd_252100366 发表于 2026/10/13 10:20:38 2026/10/13
【摘要】 从赋值、浅拷、深拷三者的内存关系图入手,分析 JSON.parse(JSON.stringify()) 流行的原因与四类数据丢失场景,给出基础版、循环引用版、WeakMap 缓存版手写实现,并与 structuredClone、lodash 做横向对比。

# JavaScript 深浅拷贝:JSON 陷阱、循环引用与四种正确姿势

> 摘要:从赋值、浅拷、深拷三者的内存关系图入手,分析 JSON.parse(JSON.stringify()) 流行的原因与四类数据丢失场景,给出基础版、循环引用版、WeakMap 缓存版手写实现,并与 structuredClone、lodash 做横向对比。
> 标签:javascript、前端

## 一、赋值、浅拷、深拷的区别

三个操作在内存里的关系完全不同:

```js
const origin = { a: 1, nested: { b: 2 } };

const assign = origin;            // 同一个对象,改 assign.nested.b 会影响 origin
const shallow = { ...origin };    // 第一层新对象,nested 仍指向原对象
const deep = JSON.parse(JSON.stringify(origin)); // 完全独立
```

判断标准只有一条:**修改拷贝结果后,原对象会不会变**。会变的是赋值或浅拷,不会变的是深拷。日常出 bug 最多的地方就是"以为自己是深拷,其实只拷了第一层"——数组里套对象、对象里套数组,然后 `push` 或改字段改到了原数据。

## 二、浅拷贝的四种方式

1. **展开运算符**:`const copy = { ...obj }`(数组用 `[...arr]`),最常用
2. **Object.assign**:`Object.assign({}, obj)`,会把原型链上的可枚举属性也拷过来
3. **slice**:`arr.slice()` 或 `arr.concat()`,仅数组
4. **Object.create + 遍历**:少见,用于带原型的场景

注意浅拷贝的共同短板:`nested` 这一层还是共享引用。

## 三、JSON.parse(JSON.stringify()) 为什么是个坑

这大概是流传最广的"深拷贝一行流",但它会静默丢数据,四类对象全军覆没:

1. **函数和 undefined**:遇到直接跳过,属性凭空消失
2. **Symbol 作为 key**:同样被忽略
3. **Date**:变成 ISO 字符串,`.getTime()` 直接报错
4. **RegExp、Map、Set**:分别变成 `{}`、`{}`、`{}`

另外遇到**循环引用**会直接抛 `TypeError: Converting circular structure to JSON`。所以它只适合"纯 JSON 数据"(后端刚返回的、确定没有上述类型)的快速深拷,生产代码不推荐。

## 四、手写深拷贝

### 基础版(递归)

```js
function deepClone(target) {
  if (typeof target !== 'object' || target === null) return target;

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

  const result = {};
  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      result[key] = deepClone(target[key]);
    }
  }
  return result;
}
```

### 循环引用版(WeakMap 缓存)

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

  // 已经拷贝过的对象直接返回缓存,打破循环
  if (cache.has(target)) return cache.get(target);

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

  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      result[key] = deepClone(target[key], cache);
    }
  }
  return result;
}
```

WeakMap 是关键:既处理了循环引用,又不会内存泄漏(键是弱引用)。用 `Reflect.ownKeys` 代替 `for...in` 还能带上 Symbol 属性。

## 五、structuredClone 与 lodash

浏览器原生方案(Chrome 98+、Node 17+):

```js
const copy = structuredClone(origin);
```

支持 Date、RegExp、Map、Set、Blob、ArrayBuffer,也处理循环引用,性能是手写递归的数倍。不支持的是函数和 DOM 节点。**能用原生就用原生**,手写版留作兼容降级或面试。

lodash 的 `cloneDeep` 是老项目常见选择,兼容性最好,代价是打包体积。

## 六、方案对比

| 方案 | 循环引用 | Date/Map/Set | 函数/Symbol | 兼容性 |
| --- | --- | --- | --- | --- |
| JSON 双步 | 报错 | 丢失 | 丢失 | 全兼容 |
| 手写基础版 | 死循环 | 丢失 | 丢失 | 全兼容 |
| 手写 WeakMap 版 | 支持 | 丢失 | 视实现 | 全兼容 |
| structuredClone | 支持 | 保留 | 丢失 | 现代浏览器 |
| lodash cloneDeep | 支持 | 保留 | 保留 | 全兼容 |

## 七、三个实践建议

1. **能不可变更新就别深拷**:React/Vue 里改嵌套对象,优先用展开逐层更新(`{ ...state, user: { ...state.user, name: 'x' } }`),比深拷更高效也更安全。
2. **框架有自带方案就用**:Vue 的 `reactive`、Immer 的 `produce`,从根上避免手动拷贝。
3. **大数据量慎用递归**:十万节点以上的深对象,递归有栈溢出风险,优先 structuredClone 或考虑结构化设计拍平数据。

## 八、总结

JSON 深拷能用但有四个明确的坑;日常首选 structuredClone,需要兼容老环境时用 WeakMap 缓存版手写;更重要的是建立"赋值≠浅拷≠深拷"的条件反射,先判断数据结构和修改深度,再选方案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。