WinddSnow

JavaScript-Proxy-and-Reflect-Metaprogramming

字数统计: 2.7k阅读时长: 11 min
2026/07/30

第61课:Proxy 与 Reflect——元编程、拦截器、响应式原理基础

ProxyReflect 是 ES6 引入的元编程工具。Proxy 允许你创建一个对象的“代理”,拦截并自定义对该对象的基本操作(如属性读取、赋值、枚举、函数调用等)。Reflect 则是一个内置对象,提供了与 Proxy 的拦截器(trap)一一对应的静态方法,用于实现默认行为,同时让操作更具函数式风格。这两者结合,不仅构成了 Vue 3 响应式系统的底层基石,也为数据校验、日志记录、API 沙箱等场景提供了强大的原生支持。本节课将详细拆解 13 种 Proxy 拦截器的用途、Reflect 的配套方法,以及如何手写一个简化版的响应式系统。


1. Proxy 基础:创建代理对象

new Proxy(target, handler) 创建一个代理对象,其中 target 是原始对象,handler 是一个包含拦截器函数的对象。所有对代理对象的操作都会被 handler 中对应的拦截器捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const target = {
name: 'Alice',
age: 25
};

const handler = {
get(target, property, receiver) {
console.log(`读取属性: ${String(property)}`);
return target[property];
},
set(target, property, value, receiver) {
console.log(`设置属性: ${String(property)} = ${value}`);
target[property] = value;
return true; // 严格模式下必须返回 true 表示成功
}
};

const proxy = new Proxy(target, handler);

proxy.name; // 控制台输出:读取属性: name
proxy.age = 30; // 控制台输出:设置属性: age = 30
console.log(target.age); // 30 —— 原始对象也被修改了

关键点:代理对象本身没有状态——它把操作“转发”给原始对象。receiver 参数指向当前被调用的代理对象(或继承代理的对象),在原型链和 getter 场景中区分 this 指向。


2. 13 种 Proxy 拦截器

Proxy 提供了 13 种拦截器,覆盖了对象所有内部方法。以下是核心类别及对应 Reflect 方法:

2.1 属性访问控制:getsethasdeleteProperty

  • **get(target, property, receiver)**:拦截属性读取。receiver 是代理对象自身或继承自代理的对象。
  • **set(target, property, value, receiver)**:拦截属性赋值。必须返回 true 表示成功,返回 false 在严格模式下抛出 TypeError
  • **has(target, property)**:拦截 in 操作符。返回布尔值。
  • **deleteProperty(target, property)**:拦截 delete 操作符。必须返回 true 表示成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const handler = {
get(target, prop) {
if (prop in target) {
return target[prop];
}
return `属性 "${String(prop)}" 不存在`;
},
set(target, prop, value) {
if (typeof value === 'number' && value < 0) {
throw new Error('值不能为负数');
}
target[prop] = value;
return true;
},
has(target, prop) {
console.log(`检查属性: ${String(prop)}`);
return prop in target;
},
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error('不能删除私有属性');
}
delete target[prop];
return true;
}
};

const obj = new Proxy({ score: 100, _internal: 42 }, handler);

console.log('score' in obj); // 检查属性: score → true
// delete obj._internal; // Error: 不能删除私有属性

2.2 原型与属性描述符:getPrototypeOfsetPrototypeOfgetOwnPropertyDescriptordefinePropertypreventExtensionsisExtensible

  • 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
2
3
4
5
6
7
8
9
const handler = {
ownKeys(target) {
// 过滤掉以 _ 开头的私有属性
return Reflect.ownKeys(target).filter(key => !String(key).startsWith('_'));
}
};

const obj = new Proxy({ name: 'Alice', _secret: 'xyz' }, handler);
console.log(Object.keys(obj)); // ['name']

2.4 函数调用与构造:applyconstruct

  • apply(target, thisArg, argumentsList):拦截函数调用(proxy(...)proxy.call(...))。
  • construct(target, argumentsList, newTarget):拦截 new proxy(...)
1
2
3
4
5
6
7
8
9
10
11
12
13
function multiply(a, b) {
return a * b;
}

const handler = {
apply(target, thisArg, args) {
console.log(`调用函数,参数: ${args}`);
return target(...args) * 2; // 结果翻倍
}
};

const proxiedMultiply = new Proxy(multiply, handler);
console.log(proxiedMultiply(3, 4)); // 调用函数,参数: 3,4 → 24

3. Reflect:默认行为的函数式封装

Reflect 是一个内置对象(不是构造函数),提供与 Proxy 拦截器一一对应的方法。它的设计目标有三个:

  • Object 上的某些内部方法(如 definePropertydeleteProperty)迁移到更“函数式”的地方,避免操作符式的语法(如 deletein)。
  • Proxy 拦截器中,通过 Reflect 调用默认行为,使代理与原始对象保持正确关联。
  • 对某些方法的返回值做出更合理的处理(如 Reflect.defineProperty 返回布尔值表示成功/失败,而 Object.defineProperty 在失败时抛出异常)。

使用 Reflect 的核心价值:在 Proxy 拦截器内部,推荐使用 Reflect 执行默认操作,而不是直接在 target 上操作。因为 Reflect 能够正确传递 receiver,确保 getter/setter 中的 this 指向代理对象,保持原型链和行为一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const parent = {
get name() { return this._name; },
set name(val) { this._name = val; }
};

const handler = {
set(target, prop, value, receiver) {
console.log(`设置: ${String(prop)} = ${value}`);
// 使用 Reflect.set 并传入 receiver,确保 setter 的 this 指向代理
return Reflect.set(target, prop, value, receiver);
}
};

const proxy = new Proxy(parent, handler);
proxy.name = 'Alice'; // 触发 set 拦截器,同时 name setter 的 this 指向 proxy

如果直接使用 target[prop] = value,setter 中的 this 将指向原始 parent 对象,而不会经过代理,后续对 proxy 的读取也不会再次触发 get 拦截,导致响应式追踪断裂。**始终使用 Reflect + receiver**。


4. 响应式原理基础:手写一个简化版 reactive

Vue 3 的响应式系统正是基于 Proxy 构建的。下面实现一个最简版本,追踪依赖并在值变化时通知更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 全局的副作用函数存储桶
const bucket = new WeakMap();
let activeEffect = null;

// 注册副作用
function effect(fn) {
activeEffect = fn;
fn(); // 立即执行一次,触发 get 以收集依赖
activeEffect = null;
}

// 跟踪依赖
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}

// 触发更新
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach(fn => fn());
}
}

// 创建响应式对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 值发生变化时触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
});
}

// ---------- 使用示例 ----------
const state = reactive({ count: 0, message: 'hello' });

effect(() => {
console.log('count 更新为:', state.count);
});

effect(() => {
console.log('message 更新为:', state.message);
});

state.count = 1; // 输出: count 更新为: 1
state.message = 'world'; // 输出: message 更新为: world
state.count = 1; // 值未变化,不触发

原理总结

  • 每当 effect 函数执行时,它内部的响应式数据读取会触发 proxygettrack 将当前 effect 注册为该属性的依赖。
  • 当属性被赋值时,proxyset 调用 trigger,通知所有依赖该属性的 effect 重新执行。
  • WeakMap 存储 target → depsMap,Map 存储 key → 依赖集合(Set)。WeakMap 使得 target 被垃圾回收时,对应的依赖能自动清理。

5. 其他 Proxy 应用场景

  • 数据校验:在 set 中验证类型、范围,拒绝非法赋值。
  • 日志与性能监控:在 get/set 中记录属性访问次数或时间。
  • 负索引数组:拦截数组的索引访问,将 arr[-1] 映射为 arr[arr.length - 1]
  • API 沙箱:限制对某些属性的访问(私有属性保护)。
  • 动态默认值:访问不存在的属性时返回默认值而非 undefined

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选)Proxyset 拦截器中,必须返回什么值以表示设置成功?
    A. undefined
    B. true
    C. false
    D. target

  2. (单选) Reflect 对象的主要用途是什么?
    A. 创建不可变对象。
    B. 提供与 Proxy 拦截器一一对应的默认操作方法。
    C. 替代 Object 的所有方法。
    D. 实现深拷贝。

  3. (填空) 拦截 in 操作符的 Proxy 陷阱是 ______

  4. (多选) 以下哪些操作可以被 Proxy 拦截?
    A. proxy.prop(属性读取)
    B. proxy.prop = value(属性赋值)
    C. 'prop' in proxyin 操作符)
    D. typeof proxytypeof 操作符)

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要编写一个 createNegativeIndexArray(arr) 函数,接收一个普通数组,返回一个支持负索引访问的 Proxy 对象。要求如下:

  • 使用 Proxy 拦截 get 操作。
  • 当索引为负数时,转换为 arr.length + index 访问(如 arr[-1] 返回最后一个元素,arr[-2] 返回倒数第二个)。
  • 正数索引和超出范围的负索引行为与普通数组保持一致(包括返回 undefined 和不抛出额外错误)。
  • 拦截 set 操作,同样支持负索引赋值。
  • 保持数组的原型链和方法可用(如 pushmap 等)。
  • 添加 JSDoc 注释说明参数和返回的代理对象的行为。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Proxygetset 拦截器逻辑、负索引转换公式。


课后练习答案

一、概念自测答案

  1. B

    • 解析:set 拦截器必须返回 true 表示设置成功,返回 false 在严格模式下抛出 TypeError
  2. B

    • 解析:Reflect 提供了与 Proxy 陷阱一一对应的静态方法,用于方便地调用默认行为并正确传递 receiver
  3. has

    • 解析:has(target, property) 拦截 in 操作符。
  4. 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
  • 代码简洁,直接输出完整代码。”
CATALOG
  1. 1. 第61课:Proxy 与 Reflect——元编程、拦截器、响应式原理基础
    1. 1.1. 1. Proxy 基础:创建代理对象
    2. 1.2. 2. 13 种 Proxy 拦截器
      1. 1.2.1. 2.1 属性访问控制:get、set、has、deleteProperty
      2. 1.2.2. 2.2 原型与属性描述符:getPrototypeOf、setPrototypeOf、getOwnPropertyDescriptor、defineProperty、preventExtensions、isExtensible
      3. 1.2.3. 2.3 属性枚举与自有属性:ownKeys
      4. 1.2.4. 2.4 函数调用与构造:apply、construct
    3. 1.3. 3. Reflect:默认行为的函数式封装
    4. 1.4. 4. 响应式原理基础:手写一个简化版 reactive
    5. 1.5. 5. 其他 Proxy 应用场景
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)