跳绳鸭
67.76M · 2026-02-04
在 Vue 的进化史中,从 Vue 2 到 Vue 3 的跨越,最核心的变革莫之过于响应式系统的重构。而这场重构的主角,正是 Object.defineProperty 与 Proxy。本文将带你从底层描述符到 Reflect 陷阱,深度拆解这两大对象代理技术。
Object.defineProperty 用于在一个对象上定义或修改属性。Vue 2 的响应式基础正是建立在其“存取描述符”之上的。
Object.defineProperty(obj, prop, descriptor);
obj:目标对象prop:要定义或修改的属性名(字符串或 Symbol)descriptor:属性描述符,是一个配置对象(包含数据描述符与存取描述符)它可分为两类,一类为数据描述符、一类为存取描述符
属性描述符不能同时包含 value/writable(数据描述符)和 get/set(存取描述符)。
数据描述符:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | any | undefined | 属性的值 |
writable | boolean | false | 是否可写(能否被重新赋值) |
enumerable | boolean | false | 是否可枚举(能否在 for...in 或 Object.keys 中出现) |
configurable | boolean | false | 是否可配置(能否被删除或修改描述符) |
存取描述符:
| 字段 | 类型 | 说明 |
|---|---|---|
get | function | 读取属性时调用的函数 |
set | function | 设置属性时调用的函数 |
无法新增/删除:必须预先定义好属性,动态添加的属性(data.b = 2)无法响应。
数组支持差:无法拦截索引修改(arr[0] = x)及 length 变更。
性能开销:必须通过递归遍历对象的所有属性进行拦截。
// 封装一个劫持对象所有属性的函数
function observe(obj) {
// 遍历对象的自有属性
Object.keys(obj).forEach((prop) => {
let value = obj[prop]; // 存储原始值
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get() {
console.log(`读取 ${prop} 属性:${value}`);
return value;
},
set(newValue) {
console.log(`给 ${prop} 赋值:${newValue}`);
value = newValue;
},
});
});
}
// 测试
const person = { name: "张三", gender: "男" };
observe(person);
person.name = "李四"; // 输出:给 name 赋值:李四
console.log(person.gender); // 输出:读取 gender 属性:男 → 男
Proxy 是ES6引入的一个新对象,用于创建一个对象的代理,从而拦截并自定义这个对象的基本操作(比如属性读取、赋值、删除、遍历等)。它是 Vue 3 实现高效响应式的基石。
语法:const proxy = new Proxy(target, handler);
target:要代理的目标对象(可以是普通对象、数组、函数,甚至是另一个 Proxy)。
handler:一个配置对象,包含多个陷阱函数(traps),每个陷阱函数对应一种对目标对象的操作(比如读取属性对应get陷阱,赋值对应set陷阱)
proxy:返回的代理对象,后续操作都通过这个代理对象进行,而非直接操作原对象。
Proxy 的强大在于它能拦截多种底层操作。
| Trap | 触发时机 | 示例 |
|---|---|---|
get(target, prop, receiver) | 读取属性时 | obj.foo |
set(target, prop, value, receiver) | 设置属性时 | obj.foo = 'bar' |
has(target, prop) | 使用in 操作符时 | 'foo' in obj |
deleteProperty(target, prop) | 删除属性时 | delete obj.foo |
ownKeys(target) | 获取自身属性名时 | Object.keys(obj) |
apply(target, thisArg, args) | 调用函数时(仅当 target 是函数) | fn() |
construct(target, args) | 使用new操作符时 | new Obejct() |
// 1. 定义原始用户对象
const user = {
name: '张三',
age: 20,
};
// 2. 创建 Proxy 代理对象
const userProxy = new Proxy(user, {
// 拦截属性读取操作(比如 userProxy.name)
get(target, prop, receiver) {
console.log(`读取属性${prop}`);
// 核心逻辑:属性不存在时返回默认提示
if (!Reflect.has(target, prop)) {
return `属性${prop}不存在`;
}
return Reflect.get(target, prop, receiver); // 用 Reflect 保证 this 指向正确
},
// 拦截属性赋值操作(比如 userProxy.age = 25)
set(target, prop, value, receiver) {
console.log(`给属性${prop}赋值:${value}`);
// 核心逻辑:属性合法性校验
switch (prop) {
case 'age':
if (typeof value !== 'number' || value <= 0) {
console.error(' 年龄必须是大于0的数字!');
return false; // 返回 false 表示赋值失败
}
break;
case 'name':
if (typeof value !== 'string' || value.trim() === '') {
console.error(' 姓名不能为空字符串!');
return false;
}
break;
}
return Reflect.set(target, prop, value, receiver); // 合法则执行赋值,返回 true 表示成功
},
});
// 3. 测试代理功能
console.log('===== 测试属性读取 =====');
console.log(userProxy.name); // 读取存在的属性
console.log(userProxy.age); // 读取存在的属性
console.log(userProxy.gender); // 读取不存在的属性
console.log('n===== 测试合法赋值 =====');
userProxy.age = 25; // 合法的年龄赋值
userProxy.name = '李四'; // 合法的姓名赋值
console.log('赋值后 name:', userProxy.name);
console.log('赋值后 age:', userProxy.age);
console.log('n===== 测试非法赋值 =====');
userProxy.age = -5; // 非法的年龄(负数)
userProxy.name = ''; // 非法的姓名(空字符串)
console.log('非法赋值后 age:', userProxy.age); // 年龄仍为 25
// 打印结果:
===== 测试属性读取 =====
读取属性name
张三
读取属性age
20
读取属性gender
属性gender不存在
===== 测试合法赋值 =====
给属性age赋值:25
给属性name赋值:李四
读取属性name
赋值后 name: 李四
读取属性age
赋值后 age: 25
114
===== 测试非法赋值 =====
给属性age赋值:-5
年龄必须是大于0的数字!
给属性name赋值:
姓名不能为空字符串!
读取属性age
非法赋值后 age: 25
Reflect 是 ES6 引入的内置全局对象,不能通过 new 实例化(不是构造函数)。它的核心作用是把原本属于 Object 对象的底层操作(比如属性赋值、删除)提炼成独立的函数方法,同时能保证操作的 “正确性”—— 比如转发操作时保留正确的 this 指向。
核心原因:处理 this 指向问题。
当对象内部存在 getter 并依赖 this 时,如果直接使用 target[prop],this 将指向原始对象而非代理对象,导致后续的属性读取无法被 Proxy 拦截。
const person = {
_name: '张三',
get name() {
console.log('getter 被调用,this:', this === person ? 'person' : this);
return this._name;
},
introduce() {
console.log('this', this)
return `我叫${this.name}`;
},
};
// 错误代理
const badProxy = new Proxy(person, {
get(target, prop, receiver) {
console.log(`拦截: ${prop}`);
if (prop === 'introduce') {
const original = target[prop]; // 错误:直接获取
return function () {
return original(); // this 指向 badProxy
};
}
return target[prop];
},
});
// 正确代理
const goodProxy = new Proxy(person, {
get(target, prop, receiver) {
console.log(`拦截: ${prop}`);
if (prop === 'introduce') {
return function () {
return Reflect.apply(target[prop], receiver, arguments); // 正确
};
}
return Reflect.get(target, prop, receiver);
},
});
console.log('=== 测试错误代理 ===');
console.log(badProxy.introduce());
console.log('n=== 测试正确代理 ===');
console.log(goodProxy.introduce()
首先执行console.log(badProxy.introduce())
badProxy.introduce属性,触发badProxy的get 陷阱,参数target = person,prop = 'introduce',receiver = badProxy接着进入badProxy的get陷阱函数,此时返回的新函数被赋值给badProxy.introduce,然后执行这个新函数。
console.log(`拦截: ${prop}`); // 输出:拦截: introduce
if (prop === 'introduce') {
const original = target[prop]; // 拿到 person.introduce 函数
return function () { // 返回一个新函数
return original(); // 关键错误:裸调用 original
};
}
执行返回的新函数original()(即person.introduce())
original是裸调用(没有对象前缀),所以introduce方法里的this指向window(非严格模式);this window;this.name→window.name,不会触发person的namegetter(因为this不是person/badProxy),所以window._name不存在,返回undefined;我叫undefined,控制台输出:我叫。执行console.log(goodProxy.introduce())
goodProxy.introduce属性,触发goodProxy的get 陷阱,参数:target = person,prop = 'introduce',receiver = goodProxy。第一次触发get陷阱(拦截introduce),此时返回的新函数被赋值给goodProxy.introduce,然后执行这个新函数
console.log(`拦截: ${prop}`); // 输出:拦截: introduce → 第一次拦截
if (prop === 'introduce') {
return function () { // 返回一个新函数
return Reflect.apply(target[prop], receiver, arguments); // 正确绑定 this
};
}
执行返回的新函数,Reflect.apply(target[prop], receiver, arguments),其中
target[prop]=person.introduce 函数;receiver=goodProxy(把introduce方法的this绑定到goodProxy);person.introduce方法,此时方法内的this = goodProxy。执行 introduce 方法内部代码
console.log('this', this); // 输出:this Proxy(Object) { _name: '张三' }(即 goodProxy)
return `我叫${this.name}`; // 关键:读取 this.name → goodProxy.name
第二次触发get陷阱(拦截name),因为this = goodProxy,所以this.name等价于goodProxy.name,需要读取goodProxy.name属性,再次触发goodProxy的get 陷阱,参数:
target = person,prop = 'name',receiver = goodProxyconsole.log(`拦截: ${prop}`); // 输出:拦截: name → 第二次拦截
if (prop === 'introduce') { /* 不执行 */ }
return Reflect.get(target, prop, receiver); // 调用 Reflect.get 读取 person.name
9. 调用Reflect.get(target, prop, receiver),触发person.name的 getter,此时 getter 里的this被receiver绑定为goodProxy
get name() {
console.log('getter 被调用,this:', this === person ? 'person' : this);
// 输出:getter 被调用,this: Proxy(Object) { _name: '张三' }
return this._name; // this = goodProxy → 读取 goodProxy._name
}
10. 返回this._name(不是name!),这时会第三次触发goodProxy的get陷阱(prop = '_name')
console.log(`拦截: ${prop}`); // 输出:拦截: _name
return Reflect.get(target, '_name', receiver); // 返回 person._name = '张三'
11. 最终返回结果 我叫张三
![]()
new 操作等。receiver 参数完美转发 this 绑定,保证了响应式系统的严密性。