深拷贝是实现可靠撤销重做的关键,因JavaScript引用传递特性,浅拷贝无法隔离嵌套对象,导致快照污染;推荐structuredClone或lodash.cloneDeep,避免JSON方案缺陷,并需在undo/redo栈操作中全程使用。

深拷贝是实现可靠撤销重做功能的关键——它确保每次操作前的状态快照彼此完全独立,避免引用共享导致的意外覆盖。
为什么必须用深拷贝?
JavaScript 中对象和数组默认是引用传递。如果只做浅拷贝(如 Object.assign() 或展开运算符),嵌套结构仍会共享内存地址。用户修改后再次撤销时,可能发现“撤销回去”的状态已经被后续操作悄悄改掉。
- 浅拷贝:顶层属性复制,子对象仍指向原内存
- 深拷贝:递归复制所有层级,生成全新、隔离的数据结构
- 撤销/重做依赖“时间点快照”,每个快照必须互不影响
推荐的深拷贝方案
不建议用 JSON.parse(JSON.stringify(obj)) —— 它会丢弃函数、undefined、Symbol、Date、RegExp 等类型,且无法处理循环引用。
- structuredClone()(现代浏览器 & Node.js 17+):原生、安全、支持 Map/Set/Date/BigInt 等,自动处理循环引用
- lodash.cloneDeep():兼容性好,健壮性强,适合需要支持旧环境的项目
- 自定义递归拷贝(仅限简单数据结构):可控但易出错,不推荐用于生产
撤销重做的基本结构设计
维护两个栈:undoStack 存已执行操作前的状态,redoStack 存被撤销的操作状态。
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
立即学习“Java免费学习笔记(深入)”;
- 每次执行操作前,用深拷贝保存当前状态到 undoStack
- 执行操作后,清空 redoStack(新操作打断重做链)
- 撤销时,从 undoStack 弹出一个状态,深拷贝后赋给当前数据,并推入 redoStack
- 重做时,从 redoStack 弹出并恢复,再推入 undoStack
一个轻量级实现示例
以编辑器状态为例:
class UndoRedoManager {
constructor(initialState) {
this.state = structuredClone(initialState);
this.undoStack = [];
this.redoStack = [];
}
execute(operation) {
this.undoStack.push(structuredClone(this.state));
this.redoStack = []; // 新操作重置重做历史
operation(this.state);
}
undo() {
if (this.undoStack.length === 0) return;
this.redoStack.push(structuredClone(this.state));
this.state = this.undoStack.pop();
}
redo() {
if (this.redoStack.length === 0) return;
this.undoStack.push(structuredClone(this.state));
this.state = this.redoStack.pop();
}
}
注意:所有入栈/出栈都使用 structuredClone,确保状态纯净。若需兼容旧环境,将 structuredClone 替换为 _.cloneDeep 即可。

















