用Object.defineProperty的getter/setter实现数据劫持,核心是不侵入原始结构、动态拦截读写并插入响应逻辑;需设configurable为true、避免与value/writable共存、用私有字段存储值、递归代理嵌套对象、WeakMap缓存防重复、注意不可枚举性及数组限制。

用属性描述符的 getter 和 setter 实现数据劫持,核心在于不侵入原始数据结构,通过 Object.defineProperty 动态拦截读写操作,从而在取值或赋值时插入响应逻辑——比如触发更新、校验、日志等。
定义响应式基础:手动绑定 getter/setter
对单个对象属性启用监听,需用 Object.defineProperty 显式设置访问器属性。注意必须将 configurable 设为 true(否则后续无法重定义),且不能与 value/writable 共存。
- 每个
getter返回内部缓存值(如_val),避免无限递归 - 每个
setter在赋新值前执行副作用(如通知依赖更新),再保存值 - 原始属性名被“代理”,真实数据建议用私有命名(如
_$$data)隔离
监听嵌套对象:递归代理 + 依赖收集雏形
单纯代理顶层属性无法响应深层变更。需在 getter 中检测返回值是否为对象——若是,递归调用代理函数并缓存结果,避免重复代理;同时可在此处埋点(如记录当前正在读取该属性,用于后续依赖追踪)。
- 判断
typeof val === 'object'且非null,再决定是否递归 - 用
WeakMap缓存已代理对象,防止循环引用和重复处理 -
setter中若新值是对象,也需递归代理后才赋值
避免常见陷阱:不可枚举性与数组限制
默认定义的访问器属性 enumerable: false,所以 for...in 或 Object.keys() 不会遍历到它——这既是保护也是隐患。若需保持原有枚举行为,需显式设 enumerable: true;而数组索引赋值(arr[0] = x)无法被常规 defineProperty 拦截,因其本质是属性操作但未预先定义描述符。
- 批量代理时,先用
Object.getOwnPropertyNames(obj)获取所有键(含不可枚举) - 数组响应化需单独处理:重写
push/pop/shift/unshift/splice等方法,或改用Proxy(ES6+ 更推荐) - 避免在
setter中直接修改同一属性(如this.xxx = newVal),否则触发自身 setter 导致死循环
轻量级封装示例:一个可复用的 reactive 函数
把上述逻辑收拢成函数,支持基本对象响应化,并预留扩展点(如传入回调函数处理变化):
function reactive(obj, onChange) {
if (typeof obj !== 'object' || obj == null) return obj;
const handler = new WeakMap();
function walk(target) {
if (handler.has(target)) return handler.get(target);
const proxy = {};
Reflect.ownKeys(target).forEach(key => {
let internalValue = target[key];
Object.defineProperty(proxy, key, {
enumerable: true,
configurable: true,
get() {
return internalValue;
},
set(newVal) {
const oldVal = internalValue;
internalValue = newVal;
onChange?.(key, oldVal, newVal);
}
});
});
handler.set(target, proxy);
return proxy;
}
return walk(obj);
}
调用 reactive(data, (key, old, newV) => console.log(`${key} changed`)) 即可获得带监听能力的代理对象。


















