导读
在 React 日常开发中,状态拷贝是高频操作。本文将系统梳理三种拷贝方式的底层机制、适用场景、性能差异及决策依据,帮助你建立清晰的选型判断体系。
一、深拷贝与浅拷贝:概念与内存模型
核心定义
| 浅拷贝 | […arr] / {…obj} / Object.assign() | 仅复制最外层容器,内部元素共享原引用 |
| 深拷贝 | _.cloneDeep / JSON.parse(JSON.stringify()) | 递归复制所有层级,生成完全独立的副本 |
内存模型图解
以对象数组为例:
const original = [{ id: 1 }, { id: 2 }];
浅拷贝 […original]:
original → [ 0x001 ] → { id: 1 } ← 两个数组元素
[ 0x002 ] → { id: 2 } ← 指向同一对象
shallow → [ 0x001 ] → { id: 1 }
[ 0x002 ] → { id: 2 }
修改 shallow[0].id 会同步影响 original[0].id。
深拷贝 _.cloneDeep(original):
original → [ 0x001 ] → { id: 1 }
[ 0x002 ] → { id: 2 }
deep → [ 0x003 ] → { id: 1 } ← 完全独立的内存空间
[ 0x004 ] → { id: 2 }
修改 deep[0].id 不影响 original[0].id。
当数组元素为基本类型(string/number)时,深浅拷贝效果一致,因为基本类型值不可变。
二、React 状态更新中的三种拷贝方案
方案一:浅拷贝(Spread 语法)
适用场景:
- 状态为基本类型数组(如 ID 列表)
- 对象数组仅需增删元素,不修改内部对象属性
- 嵌套层级 ≤ 2 层
代码示例:
// 基本类型数组 —— 增删改均安全
const newKeys = […(this.state.selectedRowKeys || [])];
newKeys.push('P004');
newKeys.pop();
newKeys.splice(1, 1);
this.setState({ selectedRowKeys: newKeys });
// 对象数组 —— 仅整体替换时安全
const newRows = […(this.state.selectedRows || [])];
newRows.push(newRecord); // 安全
newRows = newRows.filter(item => item.id !== targetId); // 安全
// 对象数组 —— 修改内部属性需特别处理
const newRows = this.state.rows.map(item =>
item.id === targetId ? { …item, status: 'updated' } : item
);
this.setState({ rows: newRows });
性能特征:O(n) 时间复杂度,仅复制数组外壳,1000 个元素耗时约 0.01ms。
方案二:深拷贝(_.cloneDeep)
适用场景:
- 需要完全独立副本的模板/配置克隆
- 撤销/重做(Undo/Redo)历史快照
- 深拷贝后不再与源数据做引用比对
代码示例:
import _ from 'lodash';
// 模板克隆 —— 编辑不影响原始模板
const template = { layout: { columns: 3 }, fields: […] };
const editable = _.cloneDeep(template);
editable.layout.columns = 4; // 不影响 template
// 历史快照
historyStack.push(_.cloneDeep(currentState));
⚠️ 风险提示:
在 selectedRows 场景中使用 _.cloneDeep 会导致引用混杂:
// ❌ 问题代码
const patientA = { id: 'P001' };
this.state = { selectedRows: [patientA] };
const newRows = _.cloneDeep(this.state.selectedRows);
newRows.push(patientB);
// newRows[0] 是 patientA 的副本(新地址)
// newRows[1] 是 patientB 的原引用(原地址)
// 引用不一致,后续 === 比对失效
性能特征:递归遍历整个树结构,1000 个对象(10属性/个)耗时约 3~5ms。
方案三:Immer(推荐用于复杂嵌套)
Immer 是 React/JS 生态中最流行的不可变状态管理库,被 Redux Toolkit、Zustand 等现代状态库内置使用。其核心机制是利用 Proxy 实现写时复制(Copy-on-Write)。
适用场景:
- 嵌套层级 ≥ 3 层的复杂 State
- 需要高频更新深层属性的场景
- 追求代码可读性和维护性的项目
代码示例:
import produce from 'immer';
// 三层嵌套修改 —— 3 行搞定
this.setState(
produce(draft => {
draft.user.info.address.city = 'Hangzhou';
draft.selectedRowKeys.push('P003');
})
);
// 对比原生展开写法 —— 13 行且易出错
this.setState({
user: {
…this.state.user,
info: {
…this.state.user.info,
address: {
…this.state.user.info.address,
city: 'Hangzhou'
}
}
}
});
底层原理:
Immer 使用 ES6 Proxy 对传入状态进行代理:
- 读取操作:无性能开销,直接返回原值
- 修改操作:仅复制从根节点到修改目标路径上的节点,未修改的分支保持原引用不变
这意味着只有真正变化的数据路径会触发组件重渲染,性能表现优于全量深拷贝。
三、方案对比总览
| 编写体验 | 浅层简洁,深层繁琐 | 简单直接 | 极度舒适(命令式语法) |
| 嵌套深层修改 | 需逐层展开,易出错 | ✅ 直接修改 | ✅ 直接修改 |
| 未修改节点引用 | ✅ 保持原引用 | ❌ 全部重建 | ✅ 仅修改路径重建 |
| 性能开销 | 极低(~0.01ms) | 较高(3~5ms) | 中等(0.5~1ms) |
| 学习成本 | 零 | 零 | 需了解 produce/Proxy 概念 |
| 适用场景 | 浅层状态、基本类型数组 | 模板克隆、历史快照 | 3层以上嵌套 State |
四、选型决策树
需要拷贝状态
│
├── 数据是基本类型数组(string/number)?
│ └── 是 → 浅拷贝 […arr] ✅
│
├── 数据是对象数组,仅增删元素,不改内部属性?
│ └── 是 → 浅拷贝 […arr] ✅
│
├── 数据是深层嵌套对象(≥3层)?
│ └── 是 → Immer produce ✅
│
└── 需要完全独立副本,彻底解耦(模板克隆/历史记录)?
└── 是 → _.cloneDeep(需评估引用比对风险)⚠️
五、性能量化参考
| 1000 个数字 | ~0.01ms | ~0.02ms | N/A |
| 1000 个对象(10属性/个) | ~0.01ms | 35ms | 0.51ms |
| 3层嵌套改深层属性 | 需手写展开 | 35ms(全量重建) | 0.51ms(仅重建修改路径) |
六、最佳实践总结
| selectedRowKeys(ID 列表) | 浅拷贝 […] | 基本类型,浅拷贝已满足不可变要求 |
| selectedRows(对象数组) | 浅拷贝 […] | 保持元素引用一致,确保 === 比对有效 |
| 深层嵌套 State(≥3层) | Immer produce | 代码简洁,性能优于全量深拷贝 |
| 模板/配置克隆 | _.cloneDeep | 需要完全独立的副本 |
| 历史快照 | _.cloneDeep | 需要某一时刻的完整状态备份 |
| 通用推荐 | Immer 统一处理复杂场景 | 兼顾可读性、正确性和性能 |


