1. 代理的核心概念与工作原理
1.1 Proxy的定义
在 JavaScript 中,代理(Proxy)是一种包装对象的机制,用于拦截对目标对象的各种操作并自定义行为。外观上,代理对象对外表现得像目标对象,但实际执行的逻辑由 handler 中的陷阱(traps)决定。通过这样的设计,开发者可以实现 属性访问、赋值、枚举等操作的拦截,而不必直接修改目标对象本身。
代理并不改变目标对象的数据结构,它只是在访问时提供一个中间层,以便把具体行为转交给自定义的处理逻辑。这种透明性使得调用方可以继续使用熟悉的对象语法,同时获得额外的控制能力。
const target = { name: 'Widget', price: 9.99 };
const handler = {get(target, prop, receiver) {console.log(`访问属性: ${prop}`);return Reflect.get(target, prop, receiver);}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 控制台输出: 访问属性: name
1.2 Trap(陷阱)的工作机制
陷阱是 代理中用于拦截原始操作的函数,例如 get、set、has、deleteProperty、ownKeys 等。每个陷阱对应一种操作,开发者可以在其中实现自定义逻辑,如日志、校验、默认值、属性不可见等场景。
当对代理执行一个操作时,JavaScript 引擎会尝试在 handler 中查找相应的陷阱并执行。如果陷阱返回了值,代理就会返回该值;如果没有相应陷阱或陷阱未返回合法结果,代理将按照默认行为继续。通过这种机制,可以实现对对象行为的全局控制。
2. Proxy的基本用法
2.1 创建Proxy对象
创建一个 Proxy 对象的基本方式是 new Proxy(target, handler)。target 是要代理的对象,handler 是一个包含陷阱的对象。代理会将对目标对象的访问委托给陷阱处理,除非陷阱提供了自定义逻辑。
在实现中,handler 可以为空对象,但为了达到实际效果,通常至少实现一个或多个陷阱。与之配合的 Reflect 提供了默认行为的实现,便于在陷阱中进行替代调用。

const target = { a: 1, b: 2 };
const handler = {get(target, prop, receiver) {if (prop === 'b') return 42;return Reflect.get(target, prop, receiver);}
};
const proxy = new Proxy(target, handler);
console.log(proxy.a); // 1
console.log(proxy.b); // 42
2.2 常用陷阱列表
常用的陷阱包括 get、set、has、deleteProperty、ownKeys、defineProperty、apply(用于函数代理)以及 construct(用于构造代理对象)。
通过组合这些陷阱,可以实现从简单的日志到复杂的权限控制、数据校验等多种场景。例如,get 可以用于只读或动态计算的属性,set 可以用于字段值的验证与约束。
const target = { name: 'Widget', age: 30 };
const handler = {set(target, prop, value) {if (prop === 'age' && (typeof value !== 'number' || value < 0)) {throw new TypeError('age 必须是非负数字');}return Reflect.set(target, prop, value);}
};
const proxy = new Proxy(target, handler);
proxy.age = 25; // 正常赋值
// proxy.age = -5; // 会抛出错误
2.3 实战:属性值校验
通过 set 陷阱对属性值进行校验,可以在写入属性之前过滤非法数据,确保对象始终处于有效状态。校验逻辑应尽量简洁且可复用,避免在陷阱中引入复杂的副作用。
示例中,对 age 的写入做了显式的类型与范围校验,未通过校验时通过抛错方式反馈调用方。这样可以直接在早期阶段阻断无效数据进入对象。
function createValidatedProxy(target, validator) {return new Proxy(target, {set(t, prop, value) {if (validator.hasOwnProperty(prop)) {const ok = validator[prop](value, prop);if (!ok) throw new TypeError(`属性 ${prop} 的值非法: ${value}`);}return Reflect.set(t, prop, value);}});
}const target = { username: 'guest', balance: 100 };
const validator = {balance: v => typeof v === 'number' && v >= 0,username: v => typeof v === 'string' && v.length > 0
};const proxy = createValidatedProxy(target, validator);
proxy.balance = 250; // OK
proxy.balance = -10; // 抛错
2.4 实战:日志拦截和数据保护
将 get 与 set 联合使用,可以实现访问日志和对敏感字段的保护。例如对敏感字段进行不可枚举处理或隐藏输出,同时记录访问轨迹以便后续分析。
通过在 get 和 has 陷阱中加入日志输出,可以实现对对象“谁在访问、访问了哪些属性”的全链路追踪。
const target = { username: 'admin', password: 'secret' };
const handler = {get(target, prop, receiver) {if (prop === 'password') {console.warn('尝试访问敏感字段 password');return undefined;}return Reflect.get(target, prop, receiver);},has(target, prop) {// 屏蔽对 password 的存在性查询if (prop === 'password') return false;return Reflect.has(target, prop);}
};
const proxy = new Proxy(target, handler);
console.log('username' in proxy); // true
console.log('password' in proxy); // false
console.log(proxy.password); // undefined
3. 实战案例合集
3.1 访问统计与日志
通过在 get 与 set 陷阱中记录操作,可以实现对对象访问的统计和分析,便于性能调优或行为审计。日志信息应简洁清晰,避免对性能造成过大影响。
该技术在前端状态管理、调试工具以及对象防篡改场景中非常有用,能够在不修改核心业务逻辑的情况下实现透明的观测能力。
const data = { value: 0 };
let hits = 0;
const proxy = new Proxy(data, {get(t, prop) {hits++;return Reflect.get(t, prop);},set(t, prop, value) {return Reflect.set(t, prop, value);}
});
proxy.value += 1;
proxy.value += 2;
console.log(`访问次数: ${hits}`); // 2
3.2 虚拟属性与只读对象
利用 get 陷阱可以实现虚拟属性,即属性在内部计算得出而不是直接存储。同时通过在 set 陷阱中阻止写入,可以实现“只读对象”的行为。
这类策略常用于 API 缓存、计算字段、以及保护客户端不越权修改敏感数据。
const target = {};
const proxy = new Proxy(target, {get(t, prop) {if (prop === 'today') {return new Date().toDateString();}return Reflect.get(t, prop);},set() {throw new TypeError('此对象为只读对象,禁止修改');}
});
console.log(proxy.today); // 根据当前日期输出
proxy.today = '2020-01-01'; // 抛错
3.3 数组操作的代理
对数组进行代理时,可以拦截数组方法如 push、pop、splice,从而实现方法级别的自定义逻辑,如约束长度、触发副作用等。
需要关注的要点包括:确保对数组下标的访问仍然返回期望的值、避免破坏原生数组行为,同时在需要时向下传递到目标对象。
const arr = [1, 2, 3];
const proxy = new Proxy(arr, {get(target, prop, receiver) {if (prop === 'push') {return function(...args) {console.log('向数组添加元素:', args);return Array.prototype.push.apply(target, args);}}return Reflect.get(target, prop, receiver);}
});
proxy.push(4); // 输出日志并添加元素
console.log(proxy); // [1, 2, 3, 4]
4. 进阶应用与注意事项
4.1 与函数结合的代理
对函数进行代理时,可以使用 apply 陷阱拦截函数调用,记录调用信息、修改参数或返回值。通过代理函数,可以在不修改原始实现的前提下实现行为增强。
此外,construct 陷阱还能拦截通过 new 关键字创建实例的过程,用于实现自定义的构造行为或实现模式如工厂函数。
function greet(name) {return `Hello, ${name}!`;
}
const handler = {apply(target, thisArg, argumentsList) {console.log(`调用 greet,参数:${argumentsList}`);return target.apply(thisArg, argumentsList).toUpperCase();}
};
const proxy = new Proxy(greet, handler);
console.log(proxy('world')); // 输出: HELLO, WORLD!
4.2 性能与兼容性注意
使用 Proxy 会带来额外的运行时开销,尤其是在高频访问的场景中。因此,仅在确有需要时才使用代理,并尽量将代理的范围限定在关键区域。
此外,旧版浏览器对 Proxy 的支持有限,在需要兼容性时需结合检测与降级策略,确保核心功能在所有目标环境中可用。
4.3 安全与错误处理
在设计代理时应明确边界,例如对敏感属性的访问进行保护、对异常情况给出明确的错误信息等。代理的陷阱应具备可控性与可预测性,避免陷阱逻辑引发不可预期的副作用。
同时,对外暴露的 API 需要文档化,说明哪些操作会被拦截、拦截的行为会如何影响对象的使用,从而降低集成成本与风险。


