核心是用 setprototypeof 为命令实例动态挂载 canvascontext 代理环境,使其具备绘图能力而不持有 canvas 实例;命令仅保存必要参数,通过原型链调用统一接口实现高效撤销重做。

用 setPrototypeOf 配合命令模式实现画布撤销,核心不是“替换原型”,而是利用它动态绑定命令对象与执行上下文,让每个命令天然持有画布状态快照能力——关键在命令实例化时的上下文注入,而非运行时篡改原型链。
命令对象需携带可序列化的执行环境
直接把画布实例传进命令构造函数易引发内存泄漏或状态不一致。更稳妥的方式是:命令只保存必要参数(如坐标、类型、旧值/新值),而通过 setPrototypeOf 在创建后“挂载”一个轻量代理环境,该环境提供 getSnapshot()、restore() 等统一接口。
- 定义一个
CanvasContext类,封装画布操作方法但不持有 canvas 实例本身 - 命令创建后立即用
Object.setPrototypeOf(command, new CanvasContext(canvas)) - 命令的
execute()和undo()内部调用this.drawRect(...)等代理方法,实际由CanvasContext转发
撤销栈中存储的是命令实例,不是原始操作
历史记录必须是完整命令对象,而非纯数据。因为 undo/redo 行为依赖具体命令逻辑(例如“填充”需还原颜色,“移动”需插值动画,“删除”需缓存被删元素)。若只存 JSON,就退化为简单状态快照,失去命令模式的解耦优势。
- 每次用户操作生成新命令实例,并调用
setPrototypeOf绑定当前画布上下文 - 将该实例 push 到
historyStack,同时清空redoStack - 撤销时调用
stack.pop().undo(),命令内部通过原型链访问到真实 canvas 操作方法
避免原型污染,用 Object.create 做安全挂载
setPrototypeOf 会修改对象原有原型,若命令类已有继承关系(比如 extends CommandBase),直接覆盖可能破坏方法查找链。推荐用组合代替强绑定:
- 命令构造函数接收 context 参数,内部用
Object.assign(this, { context }) - 需要“伪原型”行为时,改用
Object.setPrototypeOf(command, Object.create(context)) - 这样既保留命令自身属性,又让
command.drawImage能沿原型链找到 context 方法,且不干扰原有继承结构
快照不是深拷贝画布,而是记录差异
高频绘图场景下,每次操作都 toDataURL() 或 clone canvas 极其低效。命令模式的优势正在于“意图驱动”:每个命令知道自己改了什么。
- “画线”命令只存起点、终点、颜色;“擦除”命令只存区域 bounding box
- undo 时不是恢复整张图,而是反向执行一次“清除该线段”或“重绘原区域”
- 配合
setPrototypeOf提供的统一绘图接口,所有命令的 undo/redo 逻辑可复用同一套底层绘制逻辑











