Proxy 与 Reflect 元编程
普通代码"使用"语言,元编程是"修改语言的行为"。Proxy 与 Reflect 是 JavaScript 元编程的核心工具:Proxy 像给对象装了一面"拦截墙",能劫持对象的读写、枚举、调用等操作;Reflect 则提供与这些操作一一对应的函数式 API。Vue 3 的响应式、Vue 的装饰器语法,底层都依赖它们。
一、Proxy 概念:拦截对象操作
Proxy(代理) 接收两个参数:目标对象 target 与处理器 handler。此后对代理对象的操作,都会先经过 handler 中对应的**陷阱(trap)**函数:
const target = { name: "张三" };
const proxy = new Proxy(target, {
get(obj, key) {
console.log("读取属性:", key);
return obj[key];
},
});
console.log(proxy.name);
// 读取属性:name
// 张三| 术语 | 含义 |
|---|---|
| target | 被代理的目标对象 |
| handler | 定义拦截行为的对象,方法即"陷阱" |
| trap | 对应某个具体操作的回调(如 get/set) |
对 proxy 的操作不会绕过陷阱直接作用于 target,因此可以在不改动原对象的前提下注入额外逻辑。
二、可拦截操作大全
Proxy 的 13 个陷阱覆盖对象的几乎所有基本操作:
| 陷阱 | 拦截的操作 | 触发示例 |
|---|---|---|
get | 读取属性 | proxy.key |
set | 写入属性 | proxy.key = 1 |
has | in 运算符 | "key" in proxy |
deleteProperty | delete | delete proxy.key |
ownKeys | 枚举键 | Object.keys(proxy)、for...in |
getOwnPropertyDescriptor | 取属性描述符 | Object.getOwnPropertyDescriptor |
defineProperty | 定义属性 | Object.defineProperty |
apply | 函数调用 | proxy(...) |
construct | new 调用 | new proxy() |
getPrototypeOf | 读原型 | Object.getPrototypeOf(proxy) |
setPrototypeOf | 写原型 | Object.setPrototypeOf |
preventExtensions | 禁止扩展 | Object.preventExtensions |
isExtensible | 检查可扩展 | Object.isExtensible |
const p = new Proxy(
{ list: [1, 2, 3] },
{
get(t, key) {
if (key === Symbol.iterator) return undefined; // 禁用迭代
return t[key];
},
set(t, key, value) {
if (typeof value !== "number") throw new TypeError("只允许数字");
t[key] = value;
return true; // set 陷阱必须返回布尔值
},
has(t, key) {
console.log("检查 in:", key);
return key in t;
},
ownKeys(t) {
console.log("枚举键");
return Reflect.ownKeys(t);
},
apply(t, thisArg, args) {
console.log("函数被调用", args);
return t.apply(thisArg, args);
},
}
);三、Reflect 方法与 Proxy 对应关系
Reflect 提供与每个陷阱同名的静态方法,二者一一对应:
| Proxy 陷阱 | Reflect 方法 | 返回值 |
|---|---|---|
get | Reflect.get(target, key) | 属性值 |
set | Reflect.set(target, key, value) | 布尔(成功与否) |
has | Reflect.has(target, key) | 布尔 |
deleteProperty | Reflect.deleteProperty(target, key) | 布尔 |
ownKeys | Reflect.ownKeys(target) | 键数组 |
apply | Reflect.apply(fn, thisArg, args) | 调用结果 |
construct | Reflect.construct(Fn, args) | 新实例 |
getPrototypeOf | Reflect.getPrototypeOf(target) | 原型 |
setPrototypeOf | Reflect.setPrototypeOf(target, proto) | 布尔 |
defineProperty | Reflect.defineProperty(target, key, desc) | 布尔 |
preventExtensions | Reflect.preventExtensions(target) | 布尔 |
isExtensible | Reflect.isExtensible(target) | 布尔 |
getOwnPropertyDescriptor | Reflect.getOwnPropertyDescriptor(target, key) | 描述符 |
陷阱内调用同名 Reflect 方法完成"默认行为",是最标准的写法:
const proxy = new Proxy(target, {
get(t, key, receiver) {
return Reflect.get(t, key, receiver); // 转发默认读取,同时可追加逻辑
},
set(t, key, value, receiver) {
const ok = Reflect.set(t, key, value, receiver);
// 追加通知逻辑……
return ok;
},
});四、Reflect 的特点:与 Object 方法的区别
| 对比维度 | Reflect | Object |
|---|---|---|
| 调用方式 | 全部函数式 | 静态方法 + 运算符 |
| 失败反馈 | 返回布尔,不抛错 | 多数抛 TypeError |
defineProperty | Reflect.defineProperty 返回布尔 | 失败抛错 |
delete | Reflect.deleteProperty 返回布尔 | delete 静默失败(非严格模式) |
| 运算符等价 | 有对应的函数式版本 | 无(in、delete 是运算符) |
// 传统写法:失败抛错,需要 try/catch
try {
Object.defineProperty({}, "x", { value: 1 });
console.log("成功");
} catch (e) {
console.log("失败");
}
// Reflect 写法:返回布尔,直接判断
const ok = Reflect.defineProperty({}, "x", { value: 1 });
console.log(ok ? "成功" : "失败");Reflect.apply 还解决了 fn.apply 的传参限制与可读性问题:
// 传统:参数必须是数组或类数组
Math.max.apply(null, [1, 3, 2]);
// Reflect:同样接受参数数组,语义更清晰
Reflect.apply(Math.max, null, [1, 3, 2]);五、典型应用
5.1 数据校验
function createValidator(schema) {
return new Proxy(schema, {
set(target, key, value) {
if (key === "age" && (typeof value !== "number" || value < 0)) {
throw new RangeError("年龄必须是大于 0 的数字");
}
return Reflect.set(target, key, value);
},
});
}
const user = createValidator({ name: "", age: 0 });
user.age = 25; // 正常
user.age = -1; // RangeError: 年龄必须是大于 0 的数字5.2 私有属性保护
const obj = new Proxy(
{ public: 1, _secret: "密码" },
{
get(t, key) {
if (key.startsWith("_")) throw new Error("私有属性不可访问");
return Reflect.get(t, key);
},
has(t, key) {
if (key.startsWith("_")) return false; // in 也隐藏
return Reflect.has(t, key);
},
ownKeys(t) {
return Reflect.ownKeys(t).filter((k) => !String(k).startsWith("_"));
},
}
);
console.log(obj._secret); // Error: 私有属性不可访问
console.log("_secret" in obj); // false
console.log(Object.keys(obj)); // ['public']5.3 日志与审计
function withLogging(target) {
return new Proxy(target, {
get(t, key) {
const value = Reflect.get(t, key);
if (typeof value === "function") {
return (...args) => {
console.log(`调用 ${String(key)},参数:`, args);
return value.apply(t, args);
};
}
return value;
},
});
}
const api = withLogging({ add(a, b) { return a + b; } });
api.add(1, 2); // 调用 add,参数: [1, 2]5.4 默认值兜底
function withDefaults(target, defaults) {
return new Proxy(target, {
get(t, key) {
const v = Reflect.get(t, key);
return v === undefined ? defaults[key] : v;
},
});
}
const config = withDefaults({}, { retries: 3, timeout: 1000 });
console.log(config.retries); // 3(缺省值生效)六、响应式系统原理(Vue 3)
Vue 3 用 Proxy 实现响应式:读取时收集依赖,写入时触发更新。
function reactive(target) {
const deps = new Map(); // key -> 依赖集合
function track(key) {
if (activeEffect) {
let set = deps.get(key);
if (!set) deps.set(key, (set = new Set()));
set.add(activeEffect);
}
}
function trigger(key) {
const set = deps.get(key);
if (set) set.forEach((fn) => fn());
}
return new Proxy(target, {
get(t, key) {
track(key); // get 时收集当前副作用
return Reflect.get(t, key);
},
set(t, key, value) {
const ok = Reflect.set(t, key, value);
trigger(key); // set 时执行依赖
return ok;
},
});
}
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
const state = reactive({ count: 0 });
effect(() => console.log("count 变化:", state.count));
state.count = 1; // 触发 get 收集 + set 更新
// 输出:count 变化:0 → count 变化:1Reflect.get 的第三个参数 receiver 保证 this 正确,避免原型链上的属性读取出错。相比 Vue 2 的 Object.defineProperty,Proxy 能拦截新增属性、删除、in 等全部操作,且无需递归改写数组方法。
七、动态代理链
多个 Proxy 可以层层包裹,形成代理链,每层负责一类职责:
const logProxy = (t) =>
new Proxy(t, {
get(obj, k) { console.log("日志层读取", k); return Reflect.get(obj, k); },
});
const guardProxy = (t) =>
new Proxy(t, {
set(obj, k, v) { return Reflect.set(obj, k, v); },
});
const obj = { name: "Alice" };
const proxy = guardProxy(logProxy(obj)); // 先日志后守卫,职责分离
proxy.name; // 日志层读取 name注意操作经过的顺序与包裹顺序相反:最外层陷阱先执行,再逐层向内转发。
八、元编程概念与装饰器
元编程(Metaprogramming) 是编写"操作程序本身的程序"。Proxy 属于运行时元编程;装饰器属于声明式元编程——在类或方法上附加标注,编译器/运行时自动改写行为。
8.1 方法装饰器(函数式实现)
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log(`调用 ${name}:`, args);
return original.apply(this, args);
};
return descriptor;
}
class MathUtil {
add(a, b) {
return a + b;
}
}
// 等价于:log(MathUtil.prototype, "add", 描述符)8.2 类装饰器
function sealed(target) {
Object.seal(target);
Object.seal(target.prototype);
}
// @sealed 修饰类:禁止动态添加属性8.3 装饰器实现原理
Babel 转译后的装饰器核心是"高阶函数包裹":把原始方法取出,用增强函数替换:
function _applyDecorator(descriptor, decorator, target, key) {
return decorator(target, key, descriptor) || descriptor;
}| 装饰位置 | 参数 | 常见用途 |
|---|---|---|
| 类 | target(构造函数) | 扩展静态成员、冻结原型 |
| 方法 | (target, key, descriptor) | 日志、防抖、权限校验 |
| 属性 | (target, key) | 转换为响应式字段 |
原生装饰器语法(Stage 3)已进入 JavaScript 标准演进,Vue 3 的 @Component、NestJS 的 @Injectable、MobX 的 @observable 都是装饰器与 Proxy 元编程的典型应用。掌握 Proxy 与 Reflect,就掌握了在现代前端框架底层做文章的钥匙。