为什么 Proxy 是 JavaScript 最强大的特性之一
大多数 JavaScript 特性都是为业务代码设计的,但 Proxy 和 Reflect 是为库和框架设计的元编程特性。它们让 JavaScript 具备了"拦截和控制对象基本操作"的能力。
Vue 3 的响应式系统、Meteor 的数据绑定、各种 ORM 库的延迟加载和脏检查,都依赖 Proxy 实现。理解 Proxy,才能理解这些框架的底层机制。
一、Proxy 的基本概念
1.1 Proxy 的结构
const proxy = new Proxy(target, handler);
- target:被代理的目标对象(可以是任何对象,包括数组、函数、另一个 Proxy)
- handler:一个包含"陷阱"(trap)方法的对象,每个陷阱对应一个基本操作的拦截
1.2 get 陷阱
拦截属性读取:
const obj = { name: 'Alice', age: 30 };
const proxy = new Proxy(obj, {
get(target, property, receiver) {
console.log(`读取 ${property}`);
return Reflect.get(target, property, receiver);
}
});
console.log(proxy.name); // 输出: 读取 name\nAlice
1.3 set 陷阱
拦截属性写入:
const proxy = new Proxy(obj, {
set(target, property, value, receiver) {
console.log(`设置 ${property} = ${value}`);
return Reflect.set(target, property, value, receiver);
}
});
proxy.age = 31; // 输出: 设置 age = 31
1.4 has 陷阱
拦截 in 操作符:
const proxy = new Proxy(obj, {
has(target, property) {
console.log(`检查 ${property}`);
return Reflect.has(target, property);
}
});
console.log('name' in proxy); // 输出: 检查 name\ntrue
二、十三种代理陷阱
2.1 完整陷阱列表
| 陷阱 | 对应的基本操作 | 触发方式 |
|---|---|---|
| get | 属性读取 | proxy.prop / proxy[expr] |
| set | 属性写入 | proxy.prop = value |
| has | in 操作符 | property in proxy |
| deleteProperty | delete 操作符 | delete proxy.prop |
| apply | 函数调用 | proxy(...args) |
| construct | new 操作符 | new proxy(...args) |
| getPrototypeOf | 获取原型 | Object.getPrototypeOf(proxy) |
| setPrototypeOf | 设置原型 | Object.setPrototypeOf(proxy, proto) |
| isExtensible | 可扩展性检查 | Object.isExtensible(proxy) |
| preventExtensions | 阻止扩展 | Object.preventExtensions(proxy) |
| getOwnPropertyDescriptor | 属性描述符 | Object.getOwnPropertyDescriptor(proxy, prop) |
| defineProperty | 定义属性 | Object.defineProperty(proxy, prop, desc) |
| ownKeys | 属性枚举 | Object.keys(proxy) / for...in / Object.getOwnPropertyNames |
2.2 deleteProperty 陷阱
拦截 delete 操作:
const proxy = new Proxy(obj, {
deleteProperty(target, property) {
if (property.startsWith('_')) {
throw new Error(`Cannot delete private property ${property}`);
}
return delete target[property];
}
});
delete proxy._secret; // Error
delete proxy.name; // OK
2.3 apply 陷阱
让函数变得可代理:
function sum(a, b) {
return a + b;
}
const proxy = new Proxy(sum, {
apply(target, thisArg, argumentsList) {
console.log(`调用 sum(${argumentsList.join(', ')})`);
return Reflect.apply(target, thisArg, argumentsList);
}
});
console.log(proxy(1, 2)); // 输出: 调用 sum(1, 2)\n3
2.4 construct 陷阱
拦截 new 操作符:
class Person {
constructor(name) {
this.name = name;
}
}
const ProxyPerson = new Proxy(Person, {
construct(target, argumentsList, newTarget) {
console.log(`创建 Person: ${argumentsList[0]}`);
return Reflect.construct(target, argumentsList, newTarget);
}
});
const p = new ProxyPerson('Alice'); // 输出: 创建 Person: Alice
三、Reflect API
3.1 Reflect 的设计
Reflect 是一个内置对象,提供了与 Proxy 陷阱一一对应的方法。它的设计目的是:
- 提供操作对象的默认行为:Proxy 陷阱中可以调用 Reflect 方法来执行默认操作
- 让对象操作更加函数化:将
delete obj.prop变成Reflect.deleteProperty(obj, prop)
// Object vs Reflect
Object.defineProperty(obj, 'name', { value: 'Alice' });
Reflect.defineProperty(obj, 'name', { value: 'Alice' });
// 返回值不同
// Object.defineProperty 返回对象本身,成功时
// Reflect.defineProperty 返回布尔值,更符合 try/catch 模式
3.2 Reflect 方法列表
Reflect.get(target, property, receiver)
Reflect.set(target, property, value, receiver)
Reflect.has(target, property)
Reflect.deleteProperty(target, property)
Reflect.apply(target, thisArg, argumentsList)
Reflect.construct(target, argumentsList, newTarget)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, property)
Reflect.defineProperty(target, property, descriptor)
Reflect.ownKeys(target)
四、实用模式
4.1 数据验证
class Validator {
constructor(schema) {
this.schema = schema;
return new Proxy({}, {
get(target, property) {
if (!(property in schema)) {
throw new Error(`Unknown property: ${property}`);
}
return target[property];
},
set(target, property, value) {
const rules = schema[property];
if (rules.type && typeof value !== rules.type) {
throw new TypeError(`${property} must be ${rules.type}`);
}
if (rules.min !== undefined && value < rules.min) {
throw new RangeError(`${property} must be >= ${rules.min}`);
}
if (rules.max !== undefined && value > rules.max) {
throw new RangeError(`${property} must be <= ${rules.max}`);
}
target[property] = value;
return true;
}
});
}
}
const user = new Validator({
age: { type: 'number', min: 0, max: 150 },
name: { type: 'string' }
});
user.age = 25; // OK
user.age = -1; // RangeError
user.age = 'old'; // TypeError
4.2 记忆化(Memoization)
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`Cache hit for ${key}`);
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
const slowAdd = memoize((a, b) => {
// 模拟耗时计算
return a + b;
});
slowAdd(1, 2); // 计算
slowAdd(1, 2); // Cache hit
4.3 惰性属性加载
class LazyLoader {
constructor() {
this._cache = {};
}
static create(properties) {
return new Proxy({}, {
get(target, property) {
if (!(property in properties)) {
throw new Error(`Property ${property} not defined`);
}
if (!(property in target._cache)) {
const loader = properties[property];
target._cache[property] = loader();
}
return target._cache[property];
}
});
}
}
const config = LazyLoader.create({
apiUrl: () => fetch('/api/config').then(r => r.json()),
userPrefs: () => loadUserPreferences()
});
config.apiUrl.then(url => useUrl(url)); // 实际加载
4.4 实现数组负索引
const arr = [1, 2, 3, 4, 5];
const negativeIndexArray = new Proxy(arr, {
get(target, property, receiver) {
if (typeof property === 'string') {
const index = parseInt(property, 10);
if (index < 0) {
// 负索引转换为正索引
return target[target.length + index];
}
}
return Reflect.get(target, property, receiver);
}
});
console.log(negativeIndexArray[-1]); // 5
console.log(negativeIndexArray[-2]); // 4
五、Proxy 的限制
5.1 不可被代理的操作
// === 和 == 不能拦截
const p1 = new Proxy(obj, {});
const p2 = new Proxy(obj, {});
// p1 === p2 永远是 false,即使代理相同目标
// typeof 不能拦截
typeof p1; // 'object'
5.2 Proxy.revocable
const { proxy, revoke } = Proxy.revocable(target, handler);
// 使用 proxy
console.log(proxy.name);
// 撤销代理
revoke();
// 之后访问会抛出 TypeError
try {
proxy.name;
} catch (e) {
console.log('Proxy 已撤销');
}
六、面试高频考点
考点 1:Proxy 的十三种陷阱
需要能说出大部分陷阱及其对应的基本操作。
考点 2:Proxy vs Object.defineProperty
- Object.defineProperty 只能代理单个属性,Proxy 能代理整个对象
- Proxy 能拦截函数调用(apply)、构造器(construct)
- Proxy 有更好的 get/set 语义处理
考点 3:Reflect 的作用
Reflect 提供与 Proxy 陷阱对应的默认操作,使得 Proxy 陷阱中可以调用 Reflect 方法来执行默认行为。