Proxy 与 Reflect:13个陷阱与元编程完整实现
Proxy 是 JavaScript 有史以来最强大的元编程工具。它允许你在任何对象操作的底层安插钩子——不只是属性读写,还包括 in 运算符、delete、new、函数调用、原型访问等全部13种操作。Reflect 是它的镜像:每个 Proxy 陷阱都有一个 Reflect 方法提供该操作的默认行为。Vue 3 的整个响应式系统建立在这两个 API 之上。
🔹 Level 1 · 你需要知道的
Proxy 的基本结构
const proxy = new Proxy(target, handler);
- target:被代理的原始对象(任何对象,包括函数)
- handler:包含陷阱(trap)函数的对象
- proxy:返回的代理对象,所有操作都被 handler 拦截
const target = { name: 'Alice', age: 30 };
const handler = {
get(target, prop, receiver) {
console.log(`读取属性: ${prop}`);
return Reflect.get(target, prop, receiver); // 转发默认行为
},
set(target, prop, value, receiver) {
console.log(`设置属性: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
proxy.name; // 输出: 读取属性: name → 'Alice'
proxy.name = 'Bob'; // 输出: 设置属性: name = Bob
最常用的4个陷阱
get 陷阱(拦截属性读取):
// Vue 3 响应式系统的简化版
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 result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
}
});
}
has 陷阱(拦截 in 运算符):
// 实现"范围"语义:5 in range(1, 10)
function range(min, max) {
return new Proxy({}, {
has(target, prop) {
const num = Number(prop);
return !isNaN(num) && num >= min && num <= max;
}
});
}
const r = range(1, 10);
console.log(5 in r); // true
console.log(15 in r); // false
console.log('a' in r); // false
deleteProperty 陷阱(拦截 delete 运算符):
// 实现只读保护
const readOnly = new Proxy({ x: 1 }, {
deleteProperty(target, prop) {
throw new Error(`禁止删除属性: ${prop}`);
},
set(target, prop, value) {
throw new Error(`禁止修改属性: ${prop}`);
}
});
apply 陷阱(拦截函数调用):
// 函数调用日志
function logged(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`调用 ${fn.name}(${args.join(', ')})`);
const result = Reflect.apply(target, thisArg, args);
console.log(`返回: ${result}`);
return result;
}
});
}
const add = logged((a, b) => a + b);
add(3, 4); // 输出: 调用 (3, 4) → 返回: 7
Reflect 的用途
Reflect 提供的每个方法都对应一个 Proxy 陷阱,调用它可以执行该操作的默认行为:
// 在陷阱中调用 Reflect 转发默认行为
const handler = {
get(target, key, receiver) {
// 自定义逻辑...
return Reflect.get(target, key, receiver); // 转发
}
};
// Reflect 与传统 Object 方法的关键区别:
// Reflect.defineProperty 返回 boolean(不抛错)
// Object.defineProperty 返回对象或抛 TypeError
Reflect.defineProperty({}, 'x', { value: 1 }); // true(成功)
Reflect.defineProperty(Object.freeze({}), 'x', { value: 1 }); // false(失败,不抛错)
5个常见错误
错误1:直接在陷阱中操作 target 而不用 Reflect
// 危险:会导致 this 绑定错误
const handler = {
get(target, key, receiver) {
return target[key]; // 错误!用 target 而不是 receiver
}
};
// 考虑这种情况:
const proto = new Proxy({}, {
get(target, key, receiver) {
return target[key]; // 如果子对象访问继承属性,this 会指向 proto 而不是 child
}
});
const child = Object.create(proto);
child.name = 'child';
// 如果 proto 的 getter 使用 target[key],this 绑定可能错误
// 正确做法:
const handler2 = {
get(target, key, receiver) {
return Reflect.get(target, key, receiver); // receiver 正确传递
}
};
错误2:get 陷阱返回了错误的值(违反不变量)
const target = {};
Object.defineProperty(target, 'x', { value: 42, writable: false, configurable: false });
const proxy = new Proxy(target, {
get(target, key) {
return 99; // 违反不变量!
}
});
proxy.x; // TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable...
// 规范要求:对 writable:false, configurable:false 的属性,get 陷阱必须返回实际值
错误3:set 陷阱忘记返回 true
const proxy = new Proxy({}, {
set(target, key, value) {
target[key] = value;
// 忘记 return true!
}
});
proxy.x = 1; // 严格模式下报 TypeError: 'set' on proxy returned false
错误4:代理 Proxy 对象(嵌套代理触发两次)
const inner = new Proxy({}, { get(t, k) { console.log('inner get', k); return t[k]; } });
const outer = new Proxy(inner, { get(t, k) { console.log('outer get', k); return t[k]; } });
outer.x;
// 输出:outer get x(outer 拦截)
// inner get x(outer 用 t[k] 触发了 inner 的陷阱!)
// 如果用 Reflect.get(t, k, receiver),只会触发一次
错误5:Proxy.revocable 撤销后继续使用
const { proxy, revoke } = Proxy.revocable({ x: 1 }, {});
proxy.x; // 1
revoke();
proxy.x; // TypeError: Cannot perform 'get' on a proxy that has been revoked
🔸 Level 2 · 它是怎么运行的
全部13个陷阱
| 陷阱 | 拦截的内部方法 | 触发时机 |
|---|---|---|
get |
[[Get]] |
属性读取、原型链查找 |
set |
[[Set]] |
属性赋值 |
has |
[[HasProperty]] |
in 运算符 |
deleteProperty |
[[Delete]] |
delete 运算符 |
apply |
[[Call]] |
函数调用 fn() |
construct |
[[Construct]] |
new 运算符 |
getPrototypeOf |
[[GetPrototypeOf]] |
Object.getPrototypeOf、instanceof、__proto__ |
setPrototypeOf |
[[SetPrototypeOf]] |
Object.setPrototypeOf、__proto__ = |
isExtensible |
[[IsExtensible]] |
Object.isExtensible |
preventExtensions |
[[PreventExtensions]] |
Object.preventExtensions/seal/freeze |
getOwnPropertyDescriptor |
[[GetOwnProperty]] |
Object.getOwnPropertyDescriptor |
defineProperty |
[[DefineOwnProperty]] |
Object.defineProperty、属性创建 |
ownKeys |
[[OwnPropertyKeys]] |
Object.keys/values/entries、for...in、Object.getOwnPropertyNames/Symbols |
用 Proxy 实现 Vue 3 响应式核心(精简版)
以下是一个功能完整的 Vue 3 响应式核心实现,约40行:
// 全局存储:WeakMap<object, Map<key, Set<effect>>>
const targetMap = new WeakMap();
let activeEffect = null; // 当前正在执行的 effect
// 追踪依赖
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) effects.forEach(effect => effect());
}
// 创建响应式对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
track(target, key); // 读取时收集依赖
// 如果值是对象,递归代理
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 写入时触发更新
return result;
}
});
}
// 注册 effect(副作用函数)
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn(); // 执行时收集依赖
activeEffect = null;
};
effectFn(); // 立即执行一次(触发依赖收集)
return effectFn;
}
// 使用示例:
const state = reactive({ count: 0, name: 'Vue' });
effect(() => {
console.log(`count is ${state.count}`);
// 执行时读取 count → track(state, 'count') → 把这个 effect 添加到依赖
});
state.count++; // trigger(state, 'count') → 重新执行 effect → 输出 "count is 1"
state.name = 'Vue 3'; // trigger(state, 'name') → 没有订阅 name 的 effect,无输出
不变量(Invariants)系统
规范对每个 Proxy 陷阱的返回值都有约束,违反约束会抛出 TypeError。这是安全机制:
各陷阱的关键不变量:
get 陷阱:
如果 target 上该属性是 writable:false, configurable:false
→ get 必须返回与 target 上实际值相同的值
set 陷阱:
如果 target 上该属性是 writable:false, configurable:false
→ set 不能成功(必须返回 false,否则 TypeError)
has 陷阱:
如果 target 上该属性是 configurable:false
→ has 必须返回 true(不能假装属性不存在)
如果 target 不可扩展,且属性不存在
→ has 必须返回 false
deleteProperty 陷阱:
如果 target 上该属性是 configurable:false
→ deleteProperty 不能返回 true(不能假装删除成功)
ownKeys 陷阱:
如果 target 不可扩展
→ ownKeys 必须包含 target 上所有自有属性键,不能包含额外的
所有 configurable:false 的属性键都必须出现在结果中
getPrototypeOf 陷阱:
如果 target 不可扩展
→ getPrototypeOf 必须返回与 target 实际原型相同的值
isExtensible 陷阱:
返回值必须与 target 的实际 extensible 状态相同
(这是不变量中最严格的——不能撒谎)
🔺 Level 3 · 规范怎么定义的
规范 §10.5:Proxy Object Internal Methods
规范 §10.5.1 是 Proxy [[Get]] 的定义,代表了所有陷阱的典型结构:
Proxy [[Get]] (P, Receiver)(§10.5.8):
1. handler ← O.[[ProxyHandler]]
如果 handler 是 null → TypeError(代理已被撤销)
2. target ← O.[[ProxyTarget]]
3. trap ← GetMethod(handler, "get")
如果 trap 是 undefined → 返回 target.[[Get]](P, Receiver)
(没有陷阱 → 使用默认行为)
4. trapResult ← Call(trap, handler, [target, P, Receiver])
(调用陷阱函数)
5. 不变量检查(ValidateNonRevokedProxy 等):
targetDesc ← target.[[GetOwnProperty]](P)
如果 targetDesc 不是 undefined:
如果 IsDataDescriptor(targetDesc):
如果 targetDesc.[[Configurable]] 是 false
且 targetDesc.[[Writable]] 是 false:
如果 SameValue(trapResult, targetDesc.[[Value]]) 是 false
→ TypeError(不能对不可配置、不可写属性返回不同的值)
如果 IsAccessorDescriptor(targetDesc):
如果 targetDesc.[[Configurable]] 是 false
且 targetDesc.[[Get]] 是 undefined:
如果 trapResult 不是 undefined
→ TypeError(get 为 undefined 的访问器属性不能返回非 undefined)
6. 返回 trapResult
ownKeys 陷阱的精确不变量
ownKeys 陷阱的不变量是所有陷阱中最复杂的(§10.5.11):
规范约束(Proxy [[OwnPropertyKeys]]):
设 trapResultKeys = 陷阱返回的键列表
设 targetKeys = target.[[OwnPropertyKeys]]()
约束1:trapResultKeys 中没有重复键
约束2:trapResultKeys 的每个元素必须是 String 或 Symbol
如果 target 不可扩展:
约束3:trapResultKeys 必须包含 targetKeys 的所有元素
约束4:targetKeys 的每个元素必须出现在 trapResultKeys 中且只出现一次
如果 target 可扩展:
约束5:targetKeys 中所有 configurable:false 的属性必须出现在 trapResultKeys 中
// 验证不变量违反:
const target = Object.freeze({ x: 1, y: 2 }); // 不可扩展
const proxy = new Proxy(target, {
ownKeys() {
return ['x']; // 缺少 'y' — 违反约束3
}
});
Object.keys(proxy); // TypeError: 'ownKeys' on proxy: trap result did not include 'y'
// 正确的 ownKeys 用法(过滤可枚举属性):
const proxy2 = new Proxy({ a: 1, _b: 2, c: 3 }, {
ownKeys(target) {
// 只暴露不以 _ 开头的键
return Reflect.ownKeys(target).filter(k => !String(k).startsWith('_'));
},
getOwnPropertyDescriptor(target, key) {
// 必须同步修改 getOwnPropertyDescriptor,否则 Object.keys 会把这些键过滤掉
if (!String(key).startsWith('_')) {
return { ...Object.getOwnPropertyDescriptor(target, key), enumerable: true };
}
return undefined;
}
});
Object.keys(proxy2); // ['a', 'c']
defineProperty 陷阱的不变量
规范约束(Proxy [[DefineOwnProperty]]):
约束1:如果 target 不可扩展,陷阱不能返回 true 来添加不存在的属性
约束2:如果 target 上属性是 configurable:false,
陷阱不能返回 true 来修改不允许的字段:
- 不能把 configurable 改为 true
- 不能修改 enumerable
- 如果 writable:false,不能修改 value(除非 SameValue 相同)
- 不能把 writable:false 改为 true
约束3:如果 target 上属性是 configurable:false 且 writable:false,
不能以任何方式修改
💎 Level 4 · 边界与陷阱
陷阱1:Proxy 不能代理原始值
new Proxy(42, {}); // TypeError: Cannot create proxy with a non-object as target
new Proxy(null, {}); // TypeError
new Proxy('str', {}); // TypeError
new Proxy(true, {}); // TypeError
// 只能代理对象(包括函数):
new Proxy({}, {}); // OK
new Proxy([], {}); // OK
new Proxy(function(){}, {}); // OK
new Proxy(class {}, {}); // OK
// 为什么?Proxy 拦截的是对象的内部方法([[Get]]、[[Set]] 等),
// 原始值没有内部方法,无法代理
陷阱2:get 陷阱不变量的精确触发条件
这是最容易触发 TypeError 的陷阱,完整推导:
const target = {};
Object.defineProperty(target, 'frozen', {
value: 42,
writable: false,
configurable: false
});
Object.defineProperty(target, 'accessorFrozen', {
get: undefined, // get 是 undefined
configurable: false
});
const proxy = new Proxy(target, {
get(target, key) {
if (key === 'frozen') return 99; // 违反不变量!
if (key === 'accessorFrozen') return 1; // 违反不变量!(getter 是 undefined,必须返回 undefined)
return target[key];
}
});
proxy.frozen; // TypeError: 不能对不可写、不可配置属性返回不同的值
proxy.accessorFrozen; // TypeError: 访问器的 [[Get]] 为 undefined 时必须返回 undefined
// 正确处理:
const proxy2 = new Proxy(target, {
get(target, key, receiver) {
// Reflect.get 自动处理不变量
return Reflect.get(target, key, receiver);
}
});
proxy2.frozen; // 42 — 正确
陷阱3:Proxy 套 Proxy,陷阱触发两次
let getCount = 0;
const inner = new Proxy({ x: 1 }, {
get(target, key, receiver) {
getCount++;
console.log(`inner get #${getCount}: ${key}`);
return Reflect.get(target, key, receiver);
}
});
const outer = new Proxy(inner, {
get(target, key, receiver) {
getCount++;
console.log(`outer get #${getCount}: ${key}`);
return target[key]; // 错误!target 是 inner,target[key] 会触发 inner 的 get 陷阱
}
});
outer.x;
// 输出:
// outer get #1: x
// inner get #2: x
// 如果用 Reflect.get(target, key, receiver) 替代 target[key]:
// 同样会触发两次!因为 receiver 是 outer,Reflect.get 仍然转发到 inner
// 要避免触发两次,在 outer 陷阱中直接操作 innerTarget:
const innerTarget = { x: 1 };
const outerProxy = new Proxy(new Proxy(innerTarget, innerHandler), {
get(outerTarget, key, receiver) {
// 如果需要,直接读 innerTarget(绕过 inner 代理)
// 但通常嵌套代理就是为了两层都触发陷阱
}
});
嵌套代理的合法使用场景:
// 外层:日志
// 内层:验证
// 两层都应该触发是合理的
const validated = new Proxy({ x: 1 }, {
set(target, key, value) {
if (typeof value !== 'number') throw new TypeError('Only numbers allowed');
return Reflect.set(target, key, value);
}
});
const logged = new Proxy(validated, {
set(target, key, value, receiver) {
console.log(`Setting ${key} = ${value}`);
return Reflect.set(target, key, value, receiver); // 传播到 validated 的 set
}
});
logged.x = 2; // 输出日志 + 通过验证
logged.x = 'a'; // 输出日志 + 验证抛出 TypeError
陷阱4:Proxy.revocable 详解
const { proxy, revoke } = Proxy.revocable({ x: 1, y: 2 }, {
get(target, key) {
return Reflect.get(target, key);
}
});
// 使用代理
console.log(proxy.x); // 1
// 撤销代理
revoke();
// 任何操作都会 TypeError:
proxy.x; // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.x = 1; // TypeError
delete proxy.x; // TypeError
'x' in proxy; // TypeError
Object.keys(proxy); // TypeError
// revoke 可以多次调用(幂等):
revoke(); // 不报错
// 使用场景:临时权限
function withTemporaryAccess(sensitiveObj, operation) {
const { proxy, revoke } = Proxy.revocable(sensitiveObj, {});
try {
return operation(proxy);
} finally {
revoke(); // 无论如何都撤销访问权限
}
}
const secret = { token: 'secret-value' };
withTemporaryAccess(secret, (p) => {
console.log(p.token); // 'secret-value'
// operation 完成后,代理自动撤销
});
陷阱5:Reflect 与 Object 方法的精确区别
| 操作 | Reflect 版本 | Object 版本 | 关键区别 |
|---|---|---|---|
| 定义属性 | Reflect.defineProperty(obj, key, desc) → boolean |
Object.defineProperty(obj, key, desc) → obj 或 TypeError |
Reflect 失败时返回 false,Object 失败时抛错 |
| 删除属性 | Reflect.deleteProperty(obj, key) → boolean |
delete obj.key → boolean(非严格模式)或 TypeError(严格模式 + configurable:false) |
Reflect 更一致 |
| 检查属性 | Reflect.has(obj, key) → boolean |
key in obj → boolean |
功能相同 |
| 获取原型 | Reflect.getPrototypeOf(obj) |
Object.getPrototypeOf(obj) |
对非对象:Reflect 抛 TypeError,Object 在新规范中也抛 TypeError |
| 阻止扩展 | Reflect.preventExtensions(obj) → boolean |
Object.preventExtensions(obj) → obj |
返回类型不同 |
| 获取自有属性键 | Reflect.ownKeys(obj) |
Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj)) |
Reflect.ownKeys 包含所有键(String + Symbol),且按规范顺序 |
// 重要:Reflect.defineProperty vs Object.defineProperty 的错误处理
const frozen = Object.freeze({ x: 1 });
// Object 版:抛错
try {
Object.defineProperty(frozen, 'y', { value: 2 });
} catch (e) {
console.log('Object.defineProperty threw:', e.message);
}
// Reflect 版:返回 false
const success = Reflect.defineProperty(frozen, 'y', { value: 2 });
console.log('Reflect.defineProperty returned:', success); // false
// 在 Proxy 陷阱中,Reflect 版更适合:
const proxy = new Proxy({}, {
defineProperty(target, key, desc) {
// 用 Reflect 版,失败时返回 false,Proxy 层会正确处理
return Reflect.defineProperty(target, key, desc);
// 如果用 Object.defineProperty,异常可能跑到错误的调用栈
}
});
陷阱6:用 Proxy 实现"不可能的"功能
负数组索引(Python 风格的 arr[-1]):
function negativeable(arr) {
return new Proxy(arr, {
get(target, key, receiver) {
const index = Number(key);
if (!isNaN(index) && index < 0) {
key = String(target.length + index);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const index = Number(key);
if (!isNaN(index) && index < 0) {
key = String(target.length + index);
}
return Reflect.set(target, key, value, receiver);
}
});
}
const arr = negativeable([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
arr[-1] = 99;
console.log(arr); // [1, 2, 3, 4, 99]
深层只读对象(真正递归的只读):
function deepReadOnly(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
// 递归代理对象
if (typeof value === 'object' && value !== null) {
return deepReadOnly(value);
}
return value;
},
set(target, key, value) {
throw new TypeError(`Cannot set property '${String(key)}' on read-only object`);
},
deleteProperty(target, key) {
throw new TypeError(`Cannot delete property '${String(key)}' on read-only object`);
}
});
}
const config = deepReadOnly({
database: { host: 'localhost', port: 5432 },
app: { port: 3000 }
});
config.database.host; // 'localhost' — 可读
config.database.host = 'new'; // TypeError
config.database.port = 9999; // TypeError
delete config.app.port; // TypeError
自动填充默认值(自动创建嵌套路径):
function autoDefault(obj = {}) {
return new Proxy(obj, {
get(target, key) {
if (!(key in target)) {
target[key] = autoDefault(); // 自动创建子对象
}
return target[key];
}
});
}
const config = autoDefault();
config.database.host = 'localhost'; // 自动创建 config.database
config.database.port = 5432;
config.app.server.port = 3000; // 自动创建 config.app, config.app.server
console.log(config.database.host); // 'localhost'
console.log(config.app.server.port); // 3000
// 注意:这个实现会创建对象链,但不适合用于判断属性是否"真正"存在
类型安全的属性访问(运行时类型检查):
function typed(obj, schema) {
return new Proxy(obj, {
set(target, key, value) {
if (key in schema) {
const expectedType = schema[key];
if (typeof value !== expectedType) {
throw new TypeError(
`Property '${key}' must be ${expectedType}, got ${typeof value}`
);
}
}
return Reflect.set(target, key, value);
}
});
}
const user = typed({}, {
name: 'string',
age: 'number',
active: 'boolean'
});
user.name = 'Alice'; // OK
user.age = 30; // OK
user.age = '30'; // TypeError: Property 'age' must be number, got string
本章小结
-
Proxy 拦截13个对象操作,覆盖所有内部方法:从属性读写(get/set)到原型访问(getPrototypeOf),从键列举(ownKeys)到函数调用(apply/construct)。每个陷阱对应一个内部方法。
-
Reflect 是每个 Proxy 陷阱的默认实现:在陷阱中调用
Reflect.xxx(target, ...)转发默认行为,同时正确传递 receiver(this 值)。关键区别:Reflect 方法返回 boolean,对应的 Object 方法返回对象或抛错。 -
不变量是规范对陷阱的硬性约束:无论陷阱写了什么,引擎都会验证返回值。对不可写、不可配置的属性,get 陷阱必须返回实际值;对 isExtensible 陷阱,必须返回与 target 一致的值。
-
嵌套 Proxy 会使陷阱触发多次:外层代理访问内层代理时,如果用
target[key]而不是操作底层 target,内层陷阱也会触发。Proxy.revocable可创建可撤销代理,撤销后任何操作都报 TypeError。 -
Proxy 开启了真正的元编程能力:负数组索引、深层只读、自动默认值、类型运行时检查——这些在没有 Proxy 时需要大量样板代码才能实现的功能,现在可以在十几行内完成。Vue 3 的整个响应式系统正是建立在 get + set 陷阱上。