2692. 使对象不可变
2025年3月26日大约 3 分钟
2692. 使对象不可变
使用 Proxy 和 Reflect 对象进行代理,拦截修改操作
type JSONValue =
| null
| boolean
| number
| string
| JSONValue[]
| { [key: string]: JSONValue }
type Obj = Array<JSONValue> | Record<string, JSONValue>
const METHODS = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const isObject = (obj: any) => typeof obj === 'object' && obj !== null
const proxyHandler: ProxyHandler<any> = {
get: (target, props) => {
// 获取当前访问属性
const val = Reflect.get(target, props)
// 非对象返回值
if (!isObject(val)) return val
// 递归代理
return proxify(val)
},
set: (target, props) => {
//直接修改数组,抛出异常
if (Array.isArray(target)) {
throw `Error Modifying Index: ${String(props)}`
}
// 抛出对象异常修改
throw `Error Modifying: ${String(props)}`
},
}
function makeImmutable(obj: Obj): any {
return proxify(obj)
}
const methodHandler: ProxyHandler<(...args: any[]) => any> = {
apply(target) {
throw `Error Calling Method: ${target.name}`
},
}
function proxify<T extends Obj>(obj: T): T {
if (Array.isArray(obj)) {
// 遍历拦截数组操作方法
METHODS.forEach((method: (typeof METHODS)[number]) => (
(obj as any)[method] = new Proxy((obj as any)[method], methodHandler)
// 直接重写也可以
// (obj as any)[method] = ()=> { throw `Error Calling Method: ${target.name}`}
)
}
return new Proxy(obj, proxyHandler) as T
}注意点
在数组中的
如果试图调用会改变数组的方法,则会产生以下错误消息: Error Calling Method: ${methodName} 。你可以假设只有以下方法能够改变数组: ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'] 。
调用方法应该在读取方法时拦截(或者直接重写方法)
arr.push(1)
==>
const fn = arr.push;
fn(1)📊 Proxy 拦截 vs 直接重写 方法对比
| 对比项 | Proxy 方式(写法 A) | 直接重写(写法 B) |
|---|---|---|
| 性能 | ⚠️ 略慢:多了一层 Proxy 调用 | ✅ 较快:函数直接执行 |
| 扩展性 | ✅ 高:可记录日志、条件拦截、限制调用等 | ❌ 低:函数写死,只能抛错 |
| 保留原始函数信息 | ✅ 可访问 target.name 等 | ❌ 不保留原函数引用 |
| 可维护性 | ✅ 明确使用 Proxy 风格,适合统一代理逻辑 | ✅ 简洁直观,逻辑明确 |
| 内存使用 | ⚠️ 稍高:每个方法都创建一个新的 Proxy 对象 | ✅ 低:简单闭包函数 |
- 性能优先,逻辑简单时 使用 写法 B
- 需要调试日志或条件拦截时 使用 写法 A
题目要点
「2692. 使对象不可变」本质是对象结构变换问题,核心在于键空间规则、覆盖策略与深浅层处理边界。
思路拆解
- 先定义输入输出契约:哪些字段保留、哪些字段合并、冲突时谁覆盖。
- 区分基本类型、数组、普通对象,避免把不同语义混到同一分支。
- 在递归或遍历中保持结果对象的构造顺序与可预测性。
复杂度分析
- 时间复杂度:与节点总数/键总数成正相关;递归方案还要考虑层级深度。
- 空间复杂度:主要来自结果对象、递归栈与中间映射结构。
边界与测试
null、空对象、空数组、缺失字段。- 同名键冲突、嵌套层级不一致、类型不一致。
- 引用类型是否需要复制、冻结或保持原引用。
工程实践
把“冲突处理规则”写成独立函数(如 merge/resolve),比散落在分支里的临时判断更利于维护。
总结
对象题的稳定做法是先定数据契约,再写转换流程;契约清晰后,代码复杂度和错误率都会显著下降。
