Immer draft 调用 lodash.isEmpty 报 prototype 只读属性错误
Immer 11.1.9至11.1.11的constructor代理可能使Lodash对象检查抛出Proxy错误。采用含修正的正式版本;暂时无法升级时,仅对可draft字段的current快照做只读检查。
Redux Toolkit reducer 原本只是检查一个对象是否为空,调用 _.isEmpty(state.settings.options) 却抛出很长的 Proxy 错误:
TypeError: 'get' on proxy: property 'prototype' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value
若依赖中实际解析到了 Immer 11.1.9、11.1.10 或 11.1.11,应先核对这条版本分支。错误可以发生在读取 draft 的 constructor.prototype 时,尚未执行后面的状态赋值,不能仅因堆栈经过 Lodash 就把它归为业务数据损坏。
检查的是 draft,不是普通对象
Immer 给 reducer 提供的是可修改的代理对象。Lodash 的部分对象检查会读取构造器及其 prototype;11.1.11 在读取 constructor 时又套了一层代理,并为后续 prototype 读取返回另一个对象。这与代理目标上受保护属性的真实值不一致,触发了 JavaScript 的 Proxy 约束。
因此,同一份空对象在 reducer 外检查正常,进入 reducer 后异常,是有可能的。把 isEmpty 换成另一个同样读取构造器的通用比较函数,也未必避开这条路径。关闭自动冻结不等于撤掉这层 constructor 代理,不应作为处理办法。
先从实际部署产物确认 Immer 版本及其来源,例如在项目目录检查:
npm ls immer @reduxjs/toolkit
工作区可能存在多份 Immer。仅升级应用直接声明的一份,而 Redux Toolkit 仍解析到另一份旧副本,不会改变该 reducer 使用的实现。
优先采用包含修正的正式版本
Immer 11.1.12 已正式撤回这段 constructor 包装,发行记录明确包含这类回归修正;截至 2026 年 10 月 25 日,正式 11.2.9 的对应读取路径也没有这段包装。沿项目兼容范围更新实际使用的依赖,并保留锁文件变更,随后重新构建部署产物。
不必为此回退到 11.1.8,也不要在 node_modules 中全局放开原型写入或替换 Proxy 行为。这里要处理的是对象检查触发异常,不能据此推断任意原型操作都安全。依赖升级也不会代替应用对外部数据键、对象合并和补丁输入的校验。
暂时不能升级时,只对快照做检查
对于已经确定为可 draft 对象的字段,可以用 Immer 的 current() 取得当前快照,再交给只读检查。下面是 reducer 内的局部片段,沿用已有 state 和 action:
import { current } from 'immer';
import _ from 'lodash';
const optionsSnapshot = current(state.settings.options);
const wasEmpty = _.isEmpty(optionsSnapshot);
if (wasEmpty) {
state.settings.options = action.payload;
}
快照包含此次 reducer 中已经发生的修改;它不是可继续修改的 draft,也不该被当成独立深拷贝后随意写入。后续状态更新仍通过原来的 state 完成。current() 要求参数是 draft,若该字段可能为 null、原始值或新赋入的普通对象,应先按字段契约分支,不能把所有值无条件传进去。
只需要检查普通对象自身可枚举字符串键时,也可以按这个更窄的业务定义判断;但它与 Lodash 对数组、Map、Set 等对象的 isEmpty 语义不同,不能未经核对批量替换。
回归时覆盖空对象、非空对象,以及同一次 reducer 中先修改再检查的情况。确认没有 Proxy 异常,还要确认状态赋值和分支结果符合原来的业务约定。若堆栈变成字段为 null、对象已经撤销,或另一个库的代理错误,应继续按新的异常定位,不能只凭 prototype 一词认定仍是这次版本回归。


