第61课:Proxy 与 Reflect——元编程、拦截器、响应式原理基础
Proxy 和 Reflect 是 ES6 引入的元编程工具。Proxy 允许你创建一个对象的“代理”,拦截并自定义对该对象的基本操作(如属性读取、赋值、枚举、函数调用等)。Reflect 则是一个内置对象,提供了与 Proxy 的拦截器(trap)一一对应的静态方法,用于实现默认行为,同时让操作更具函数式风格。这两者结合,不仅构成了 Vue 3 响应式系统的底层基石,也为数据校验、日志记录、API 沙箱等场景提供了强大的原生支持。本节课将详细拆解 13 种 Proxy 拦截器的用途、Reflect 的配套方法,以及如何手写一个简化版的响应式系统。
1. Proxy 基础:创建代理对象
new Proxy(target, handler) 创建一个代理对象,其中 target 是原始对象,handler 是一个包含拦截器函数的对象。所有对代理对象的操作都会被 handler 中对应的拦截器捕获。
1 | const target = { |
关键点:代理对象本身没有状态——它把操作“转发”给原始对象。receiver 参数指向当前被调用的代理对象(或继承代理的对象),在原型链和 getter 场景中区分 this 指向。
2. 13 种 Proxy 拦截器
Proxy 提供了 13 种拦截器,覆盖了对象所有内部方法。以下是核心类别及对应 Reflect 方法:
2.1 属性访问控制:get、set、has、deleteProperty
- **
get(target, property, receiver)**:拦截属性读取。receiver是代理对象自身或继承自代理的对象。 - **
set(target, property, value, receiver)**:拦截属性赋值。必须返回true表示成功,返回false在严格模式下抛出TypeError。 - **
has(target, property)**:拦截in操作符。返回布尔值。 - **
deleteProperty(target, property)**:拦截delete操作符。必须返回true表示成功。
1 | const handler = { |
2.2 原型与属性描述符:getPrototypeOf、setPrototypeOf、getOwnPropertyDescriptor、defineProperty、preventExtensions、isExtensible
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy)和__proto__。setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto)。返回布尔值。getOwnPropertyDescriptor(target, prop):拦截Object.getOwnPropertyDescriptor(proxy, prop)。defineProperty(target, prop, descriptor):拦截Object.defineProperty(proxy, prop, descriptor)和Object.defineProperties。返回布尔值。preventExtensions(target):拦截Object.preventExtensions(proxy)。返回布尔值。isExtensible(target):拦截Object.isExtensible(proxy)。返回布尔值。
2.3 属性枚举与自有属性:ownKeys
ownKeys(target) 拦截以下操作:Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) 和 Reflect.ownKeys(proxy)。必须返回一个可枚举的字符串或 Symbol 数组。
1 | const handler = { |
2.4 函数调用与构造:apply、construct
apply(target, thisArg, argumentsList):拦截函数调用(proxy(...)或proxy.call(...))。construct(target, argumentsList, newTarget):拦截new proxy(...)。
1 | function multiply(a, b) { |
3. Reflect:默认行为的函数式封装
Reflect 是一个内置对象(不是构造函数),提供与 Proxy 拦截器一一对应的方法。它的设计目标有三个:
- 将
Object上的某些内部方法(如defineProperty、deleteProperty)迁移到更“函数式”的地方,避免操作符式的语法(如delete、in)。 - 在
Proxy拦截器中,通过Reflect调用默认行为,使代理与原始对象保持正确关联。 - 对某些方法的返回值做出更合理的处理(如
Reflect.defineProperty返回布尔值表示成功/失败,而Object.defineProperty在失败时抛出异常)。
使用 Reflect 的核心价值:在 Proxy 拦截器内部,推荐使用 Reflect 执行默认操作,而不是直接在 target 上操作。因为 Reflect 能够正确传递 receiver,确保 getter/setter 中的 this 指向代理对象,保持原型链和行为一致性。
1 | const parent = { |
如果直接使用 target[prop] = value,setter 中的 this 将指向原始 parent 对象,而不会经过代理,后续对 proxy 的读取也不会再次触发 get 拦截,导致响应式追踪断裂。**始终使用 Reflect + receiver**。
4. 响应式原理基础:手写一个简化版 reactive
Vue 3 的响应式系统正是基于 Proxy 构建的。下面实现一个最简版本,追踪依赖并在值变化时通知更新。
1 | // 全局的副作用函数存储桶 |
原理总结:
- 每当
effect函数执行时,它内部的响应式数据读取会触发proxy的get,track将当前effect注册为该属性的依赖。 - 当属性被赋值时,
proxy的set调用trigger,通知所有依赖该属性的effect重新执行。 WeakMap存储 target → depsMap,Map存储 key → 依赖集合(Set)。WeakMap使得target被垃圾回收时,对应的依赖能自动清理。
5. 其他 Proxy 应用场景
- 数据校验:在
set中验证类型、范围,拒绝非法赋值。 - 日志与性能监控:在
get/set中记录属性访问次数或时间。 - 负索引数组:拦截数组的索引访问,将
arr[-1]映射为arr[arr.length - 1]。 - API 沙箱:限制对某些属性的访问(私有属性保护)。
- 动态默认值:访问不存在的属性时返回默认值而非
undefined。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在
Proxy的set拦截器中,必须返回什么值以表示设置成功?
A.undefined
B.true
C.false
D.target(单选)
Reflect对象的主要用途是什么?
A. 创建不可变对象。
B. 提供与Proxy拦截器一一对应的默认操作方法。
C. 替代Object的所有方法。
D. 实现深拷贝。(填空) 拦截
in操作符的Proxy陷阱是______。(多选) 以下哪些操作可以被
Proxy拦截?
A.proxy.prop(属性读取)
B.proxy.prop = value(属性赋值)
C.'prop' in proxy(in操作符)
D.typeof proxy(typeof操作符)
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 createNegativeIndexArray(arr) 函数,接收一个普通数组,返回一个支持负索引访问的 Proxy 对象。要求如下:
- 使用
Proxy拦截get操作。 - 当索引为负数时,转换为
arr.length + index访问(如arr[-1]返回最后一个元素,arr[-2]返回倒数第二个)。 - 正数索引和超出范围的负索引行为与普通数组保持一致(包括返回
undefined和不抛出额外错误)。 - 拦截
set操作,同样支持负索引赋值。 - 保持数组的原型链和方法可用(如
push、map等)。 - 添加 JSDoc 注释说明参数和返回的代理对象的行为。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Proxy 的 get 和 set 拦截器逻辑、负索引转换公式。
课后练习答案
一、概念自测答案
B
- 解析:
set拦截器必须返回true表示设置成功,返回false在严格模式下抛出TypeError。
- 解析:
B
- 解析:
Reflect提供了与Proxy陷阱一一对应的静态方法,用于方便地调用默认行为并正确传递receiver。
- 解析:
has- 解析:
has(target, property)拦截in操作符。
- 解析:
A、B、C
- 解析:
Proxy可拦截属性读取、赋值、in操作符。typeof操作符不能被Proxy拦截,它直接返回'object'(除非目标本身是函数,则返回'function')。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数createNegativeIndexArray(arr),使用Proxy创建一个支持负索引访问的数组代理。要求:
get拦截器:如果prop是数字字符串且为负数,转换为arr.length + Number(prop);否则使用Reflect.get返回默认值。set拦截器:同样处理负索引,使用Reflect.set设置值,并返回true。- 使用
const声明函数,添加 JSDoc 注释。- 提供使用示例:
const arr = createNegativeIndexArray([10,20,30]); console.log(arr[-1]); // 30; arr[-1] = 99; console.log(arr[2]); // 99。- 代码简洁,直接输出完整代码。”