状态机需深拷贝以确保状态不可变,防止引用共享导致污染;推荐 structuredClone、Immer 或轻量递归拷贝,避免 JSON 方案缺陷,并注意函数、日期等特殊类型处理。

在状态机中做深拷贝,核心目标是让每次状态变更都产生一个全新、独立、不可变的对象副本,避免意外的引用共享导致状态污染。
为什么状态机需要深拷贝
状态机(如 XState 或自定义 FSM)依赖状态的不可变性来触发重渲染、回溯、日志记录或时间旅行调试。如果只是浅拷贝(Object.assign 或展开运算符),嵌套对象/数组仍会共享引用——修改后续状态可能“悄悄”改掉之前的状态快照。
例如:
状态{ user: { name: "Alice", prefs: { theme: "dark" } } },若仅浅拷贝后修改 nextState.user.prefs.theme = "light",原始状态里的 prefs 也会变。推荐的深拷贝方式(兼顾安全与实用性)
不建议用 JSON.parse(JSON.stringify(obj)):它会丢弃函数、undefined、Symbol、Date、RegExp、Map/Set 等,且无法处理循环引用。
立即学习“Java免费学习笔记(深入)”;
更稳妥的做法是:
-
使用结构化克隆(现代浏览器 & Node.js 18.15+):
structuredClone(state)是原生、高效、支持大多数内置类型(包括 Map、Set、Date、RegExp、ArrayBuffer)的安全方案,且自动处理循环引用。 -
搭配 immer(推荐用于复杂业务状态机):用
produce(state, draft => { /* 修改 draft */ }),底层自动深拷贝 + 按需代理,语义清晰,性能好,还能保留函数和特殊对象(需配置)。 - 轻量级自定义深拷贝(无依赖场景):对纯 POJO(Plain Old JavaScript Objects)和数组递归复制,跳过函数、原型、不可枚举属性即可。适合状态结构简单、可控的 FSM。
在状态机 transition 中正确使用示例
以简易 FSM 为例,每次 transition 都返回新状态副本:
function createMachine(config) {
let currentState = structuredClone(config.initialState);
<p>return {
getState: () => structuredClone(currentState), // 外部只读访问
send: (event) => {
const nextState = config.transitions[currentState.status]?.[event];
if (nextState) {
// 关键:深拷贝 + 合并变更(或用 immer produce)
currentState = structuredClone(nextState);
// 或:currentState = produce(currentState, draft => {
// Object.assign(draft, nextState);
// });
}
}
};
}注意:若 nextState 是部分更新(如只改 user.name),应基于原状态深拷贝后再赋值,而非直接替换整个对象——这样才能保留未改动字段的深层结构完整性。
特别注意不可变性的边界
深拷贝解决的是「对象层级」的不可变;但若状态中包含外部传入的函数、类实例或第三方库对象(如 moment 实例、canvas context),它们本身不是不可变的。此时需:
- 避免把可变对象放入状态;优先存序列化数据(如
date: "2024-05-20"而非new Date()) - 必要时在拷贝逻辑中单独处理特定类型(如
instanceof Date ? new Date(date.getTime()) : ...) - 用 TypeScript 类型约束 + 运行时校验,提前拦截非法状态值


















