Appearance
✨ 数据拦截的本质 👌
要点速览
- 数据拦截指的是在“读/写/删除/查看原型等操作”中途打断,插入自定义逻辑。
- 两条主线:
Object.defineProperty(Vue 1/2) vsProxy(Vue 3)。 - 广度差异:
defineProperty拦截“特定属性的读写”;Proxy拦截“整个对象的多种操作”。 - 深度拦截都可实现,但
defineProperty需预遍历属性且新增属性不生效;数组需额外处理。 - 性能结论要看场景:少量属性、特定点拦截时
defineProperty可能够用;全面拦截更适合Proxy。
快速上手
下面用最小例子直观理解“拦截”的含义,以及两种实现方式的差异。
js
// 方式一:Object.defineProperty - 拦截特定属性的读写
const obj = {};
let _name = "张三";
Object.defineProperty(obj, "name", {
get() {
console.log("读取 name 被拦截");
return _name;
},
set(value) {
console.log("设置 name 被拦截", value);
_name = typeof value === "number" ? "张三" : value;
},
enumerable: true,
configurable: true,
});
console.log(obj.name);
obj.name = "李四";
console.log(obj.name);js
// 方式二:Proxy - 针对整个对象,拦截更广泛的操作
const target = { name: "张三" };
const p = new Proxy(target, {
get(obj, prop) {
console.log(`get:${String(prop)}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`set:${String(prop)} ->`, value);
obj[prop] = typeof value === "number" ? "张三" : value;
return true;
},
});
console.log(p.name); // 张三
p.name = "李四";
console.log(p.name); // 李四认知与前提
Proxy并非“天然更快”,实际取舍取决于“拦截范围”和“操作种类”。- IE 等老环境不支持
Proxy且不可 polyfill;需要降级策略或编译时方案。 - 如果只需在少量、固定属性上做简单拦截,
defineProperty也能胜任。
核心概念
什么是“拦截”?
- 在“操作数据的过程”中插入自定义逻辑:例如读值前记录日志、写值前做校验、删除属性前做权限检查、查看原型时统计调用等。
- 在框架中,这种能力用于响应式依赖收集、变更调度、DevTools 探测等。
两种实现主线
Object.defineProperty(obj, prop, descriptor):为“特定属性”添加访问器(getter/setter),拦截读/写操作。new Proxy(target, handler):针对“整个对象”,可拦截多种操作(get/set/deleteProperty/has/getPrototypeOf/setPrototypeOf/apply/construct等)。
js
// defineProperty 的典型形态:属性级拦截
Object.defineProperty(obj, prop, {
get() {
/* 读时拦截 */
},
set(v) {
/* 写时拦截 */
},
});
// Proxy 的典型形态:对象级拦截
new Proxy(target, {
get(obj, prop) {
/* 读任何属性时拦截 */
},
set(obj, prop, v) {
/* 写任何属性时拦截 */
},
deleteProperty(obj, prop) {
/* 删除属性时拦截 */
},
// ...更多 handler
});对象字面量/类:直接写 get/set 访问器
除了通过 defineProperty 显式定义访问器外,还可以在“对象字面量”或“类”里直接声明 get / set,本质上也是为“特定属性”提供拦截逻辑。
js
// 对象字面量:为特定属性定义访问器
const user = {
_name: "张三",
get name() {
console.log("读取 name 被拦截");
return this._name;
},
set name(v) {
console.log("设置 name 被拦截", v);
this._name = typeof v === "number" ? "张三" : v;
},
};
console.log(user.name);
user.name = "李四";
console.log(user.name);js
// 类:为实例属性提供访问器
class Student {
constructor() {
this._name = "张三";
}
get name() {
console.log("读取 name 被拦截");
return this._name;
}
set name(v) {
console.log("设置 name 被拦截", v);
this._name = typeof v === "number" ? "张三" : v;
}
}
const s = new Student();
console.log(s.name);
s.name = "李四";
console.log(s.name);与 defineProperty 的关系
- 语法更简洁,但本质仍是“属性级拦截”的访问器;等价于对该属性做
defineProperty的访问器描述符。 - 只能作用于“声明的那些属性”,无法覆盖新增属性、删除属性、原型相关、函数调用等更广泛的操作。
- 无法统一拦截数组索引写入与长度变化;复杂结构与动态字段仍更适合
Proxy。
框架中的用途
- Vue 1/2:以
defineProperty为基础实现响应式,需要预遍历对象、为数组方法做“包裹”。 - Vue 3:以
Proxy为基础,天然支持新增/删除属性拦截,设计更简洁,覆盖更全面。
深度拦截与数组差异
defineProperty 的深度拦截
defineProperty 做深度拦截需要“递归遍历 + 逐属性定义访问器”,新增属性默认不受控:
js
// 预遍历对象:为每个现有属性定义访问器(新增属性默认不受控)
function deepDefineProperty(obj) {
for (const key of Object.keys(obj)) {
const val = obj[key];
// 若是对象/数组,先递归处理其子属性
if (val && typeof val === "object") {
deepDefineProperty(val);
}
// 使用闭包变量作为属性的真实存储,避免 getter/setter 递归
let _value = val;
Object.defineProperty(obj, key, {
get() {
console.log("读取属性", String(key));
return _value;
},
set(value) {
console.log("设置属性", String(key), value);
// 如果新值是对象/数组,递归为其子属性也做拦截
if (value && typeof value === "object") {
deepDefineProperty(value);
}
_value = value; // 只更新闭包值,避免触发自身 setter
},
enumerable: true,
configurable: true,
});
}
}常见坑:新增属性与数组
- 新增属性不会被既有访问器拦截,需要再次
defineProperty。 - 数组的“索引写入”和“长度变化”无法通过单纯属性访问器可靠拦截。
- Vue 2 通过“包裹数组变更方法”(如
push/splice/sort)实现拦截与通知。
Proxy 的深度拦截
Proxy 可在 get 中按需“惰性地返回子对象的代理”,从而实现深度拦截:
js
function deepProxy(obj) {
return new Proxy(obj, {
get(o, prop) {
const val = o[prop];
console.log(`get:${String(prop)}`);
return val && typeof val === "object" ? deepProxy(val) : val;
},
set(o, prop, v) {
console.log(`set:${String(prop)}`);
o[prop] = v;
return true;
},
deleteProperty(o, prop) {
console.log(`delete:${String(prop)}`);
return delete o[prop];
},
});
}惰性代理与缓存
- 为避免重复创建代理,可用
WeakMap缓存已代理对象:当get返回子对象时先查缓存。 - 这样既保留“惰性”优势,又避免频繁的代理包装开销。
广度与能力:差异对比
| 维度 | defineProperty | Proxy |
|---|---|---|
| 拦截范围 | 特定属性 | 整个对象(所有属性) |
| 可拦截操作 | get/set | get/set/delete/has/原型相关/函数调用等 |
| 新增/删除属性拦截 | 需额外处理(默认不拦截) | 天然支持 |
| 数组方法 | 需包裹变更方法 | 直接在 set/get 中处理即可 |
| 深度拦截 | 需预遍历递归 | 可惰性递归 + 缓存 |
| 兼容性 | 广(老环境可用) | 现代浏览器/Node,IE 不支持 |
何时选哪一个?
- 新项目、现代环境、需要全面拦截:优先
Proxy(更简单更全面)。 - 老旧环境或只需在少量属性上做简单拦截:
defineProperty亦可满足。
性能与取舍
- 性能没有“一刀切”的答案:关键在“拦截操作的种类与范围”。
- 少量、固定属性拦截:
defineProperty足够且开销小。 - 复杂对象、动态属性增删、数组大量操作:
Proxy更合适,代码更简洁,维护成本更低。
经验法则
- 代码复杂度与维护成本,也是“性能”外的重要考量。
- 在响应式框架设计里,
Proxy的覆盖面与简洁性通常更能降低长期成本。
实战与陷阱
defineProperty:新增属性未被拦截
js
const obj = { a: 1 };
deepDefineProperty(obj);
obj.b = 2; // 直接新增,不会触发既有访问器
console.log(obj.b); // 未拦截读取解决思路:新增后再 defineProperty;或在业务层避免动态结构变化;或切换到 Proxy。
defineProperty:数组索引与长度
js
const arr = [1, 2, 3];
// 直接 arr[1] = 99; 或 arr.length = 1; 并不受 defineProperty 的统一拦截
// Vue 2 通过包裹 push/splice/sort 等方法,实现拦截与通知Proxy:惰性代理重复创建
js
// 使用 WeakMap 缓存,避免重复创建代理;仅对对象进行代理
const cache = new WeakMap();
function getProxy(obj) {
if (!obj || typeof obj !== "object") return obj;
if (cache.has(obj)) return cache.get(obj);
const p = new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
console.log(`get:${String(prop)}`);
// 惰性代理:仅在读取到子对象时为其创建代理
return value && typeof value === "object" ? getProxy(value) : value;
},
set(target, prop, value, receiver) {
console.log(`set:${String(prop)} ->`, value);
// 保持对象不变式:用 Reflect.set 执行真实赋值,并返回布尔结果
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop) {
console.log(`delete:${String(prop)}`);
return Reflect.deleteProperty(target, prop);
},
has(target, prop) {
console.log(`has:${String(prop)}`);
return Reflect.has(target, prop);
},
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target);
},
setPrototypeOf(target, proto) {
return Reflect.setPrototypeOf(target, proto);
},
});
cache.set(obj, p);
return p;
}兼容性注意
Proxy不支持 IE,且无法 polyfill;如需兼容需采用降级策略或编译时生成命令式更新代码(类似 Svelte/部分 Vapor 路线)。
使用建议
- 新项目用
Proxy,获得更全面的拦截能力与更低的维护复杂度。 - 老项目或需要兼容 IE:保持
defineProperty,并合理地为数组方法做包裹、为新增属性做二次定义。 - 做深度拦截时优先“惰性 + 缓存”(
Proxy + WeakMap),减少开销。 - 拦截里只做必要逻辑:记录、校验、调度;避免在拦截器中做重活,导致性能回退。
小结与后续
- 数据拦截的本质是“在操作中插入自定义逻辑”,服务于响应式与可观测性。
defineProperty适合属性级、固定结构的拦截;Proxy适合对象级、动态结构的全面拦截。- 选择的关键在场景与长期成本;必要时结合编译期方案,兼顾体验与性能。
