Proxy 是 ES6 引入的元编程特性,简单说就是给对象 “装一层拦截器”—— 所有对对象的操作(比如读属性、改属性、删属性),都会先经过这层拦截器,我们可以在拦截器里自定义操作逻辑。它的灵活性极高,是 Vue3 响应式、数据验证、API 拦截等场景的核心技术。
关联知识
可以将 Proxy 和 Reflect 合并在一起学习~
一、Proxy 的核心:三要素与基础用法
Proxy 的使用很直观,核心是 new Proxy(target, handler) 这个构造函数,需要传入两个关键参数:
1 2
| const proxy = new Proxy(target, handler);
|
- target:被代理的 “目标对象”(可以是普通对象、数组、函数,甚至另一个代理)。
- handler:“拦截器配置对象”,里面定义了各种 “拦截方法”(比如
get 拦截读操作,set 拦截写操作)。
- 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
| const user = { name: 'Alice', age: 28 };
const userHandler = { get(target, propKey) { console.log(`正在读取属性:${propKey}`); return Reflect.get(target, propKey); }, set(target, propKey, value) { console.log(`正在修改属性 ${propKey}:从 ${target[propKey]} 改成 ${value}`); return Reflect.set(target, propKey, value); }, };
const proxyUser = new Proxy(user, userHandler);
console.log(proxyUser.name); proxyUser.age = 29;
|
二、13 种核心拦截方法:覆盖所有对象操作
Proxy 提供了 13 种拦截方法,覆盖了对象的几乎所有基础操作。我整理了日常开发中最常用的几种,按使用频率排序:
| 拦截器方法 |
触发时机 |
示例 |
核心作用 |
get(target, propKey, receiver) |
读取对象属性时 |
proxy.name、proxy[0] |
监控属性读取、返回自定义值(比如默认值) |
set(target, propKey, value, receiver) |
设置对象属性时 |
proxy.age = 30 |
验证属性值、监控属性修改 |
has(target, propKey) |
使用 in 操作符时 |
'name' in proxy |
自定义 “属性是否存在” 的判断逻辑 |
deleteProperty(target, propKey) |
使用 delete 操作时 |
delete proxy.age |
监控属性删除、阻止敏感属性删除 |
apply(target, thisArg, args) |
代理的目标是函数,且函数被调用时 |
proxy(1, 2) |
拦截函数调用、修改参数或返回值 |
construct(target, args) |
代理的目标是构造函数,且用 new 创建实例时 |
new proxy(1, 2) |
拦截实例创建、修改实例属性 |
ownKeys(target) |
遍历对象属性时(如 Object.keys(proxy)、for...in) |
Object.keys(proxy) |
自定义遍历返回的属性列表(比如隐藏敏感属性) |
其他拦截方法多用于底层元编程,日常开发中较少直接使用,了解即可。
- getOwnPropertyDescriptor(target, propKey)
- defineProperty(target, propKey, propDesc)
- preventExtensions(target)
- getPrototypeOf(target)
- isExtensible(target)
- setPrototypeOf(target, proto)
三、Proxy 的实战场景:这些地方用它最香
3.1 响应式数据(Vue3 核心原理)
Vue3 的响应式系统就是基于 Proxy 实现的 —— 通过拦截对象的 get(收集依赖)和 set(触发更新),实现 “数据变,视图自动变”。
简化版实现如下:
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
| const reactiveMap = new WeakMap();
function track(target, propKey) { if (!reactiveMap.has(target)) { reactiveMap.set(target, new Map()); } const propMap = reactiveMap.get(target); if (!propMap.has(propKey)) { propMap.set(propKey, new Set()); } const updateFn = () => console.log(`属性 ${propKey} 变了,更新视图!`); propMap.get(propKey).add(updateFn); }
function trigger(target, propKey) { if (!reactiveMap.has(target)) return; const propMap = reactiveMap.get(target); if (propMap.has(propKey)) { propMap.get(propKey).forEach((fn) => fn()); } }
function reactive(target) { if (reactiveMap.has(target)) return reactiveMap.get(target);
const proxy = new Proxy(target, { get(target, propKey, receiver) { track(target, propKey); const value = Reflect.get(target, propKey, receiver); return typeof value === 'object' && value !== null ? reactive(value) : value; }, set(target, propKey, value, receiver) { const oldValue = Reflect.get(target, propKey, receiver); const success = Reflect.set(target, propKey, value, receiver); if (oldValue !== value) { trigger(target, propKey); } return success; }, });
reactiveMap.set(target, proxy); return proxy; }
const user = reactive({ name: 'Bob', address: { city: 'Beijing' } }); user.name = 'Charlie'; user.address.city = 'Shanghai';
|
3.2 数据验证:确保属性值符合规则
比如要求 age 必须是正整数,name 不能是空字符串 —— 通过 set 拦截器就能实现:
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 validatorHandler = { set(target, propKey, value) { switch (propKey) { case 'age': if (!Number.isInteger(value) || value < 0) { throw new TypeError(`年龄 ${value} 无效!必须是正整数`); } break; case 'name': if (typeof value !== 'string' || value.trim() === '') { throw new TypeError('姓名不能为空!'); } break; } return Reflect.set(target, propKey, value); }, };
const person = new Proxy({}, validatorHandler);
person.age = 30; person.name = 'David';
person.age = 'thirty'; person.name = '';
|
3.3 API 请求拦截:统一管理接口调用
比如给所有 API 调用加 “请求日志” 和 “基础 URL 拼接”,不用每次调用都写重复逻辑:
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
| const apiConfig = { baseURL: 'https://api.example.com' };
const apiHandler = { get(target, endpoint) { return async (params) => { console.log(`调用 API:${endpoint},参数:`, params); const url = `${target.baseURL}/${endpoint}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }); return response.json(); }; }, };
const api = new Proxy(apiConfig, apiHandler);
const userData = await api.users({ id: 123 }); const orderData = await api.orders({ userId: 123 });
|
3.4 自动持久化:修改数据自动存到本地存储
比如让配置数据修改后自动保存到 localStorage,刷新页面也不会丢:
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
| function createPersistentState(storageKey, initialState) { const storedData = localStorage.getItem(storageKey); const state = storedData ? JSON.parse(storedData) : initialState;
const handler = { set(target, propKey, value) { const success = Reflect.set(target, propKey, value); localStorage.setItem(storageKey, JSON.stringify(target)); return success; }, };
return new Proxy(state, handler); }
const appSettings = createPersistentState('app-settings', { theme: 'light', fontSize: 16, });
appSettings.theme = 'dark'; appSettings.fontSize = 18;
|
3.5 数组变化监听:完美支持数组方法
Proxy 能原生拦截数组的 push、pop、splice 等方法,不用像 Object.defineProperty 那样做特殊 hack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const arrayHandler = { get(target, propKey) { const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice']; if (mutationMethods.includes(propKey)) { return function (...args) { console.log(`数组执行 ${propKey},参数:`, args); return Array.prototype[propKey].apply(target, args); }; } return Reflect.get(target, propKey); }, };
const list = new Proxy([1, 2, 3], arrayHandler); list.push(4); list.splice(0, 1);
|
四、Proxy 实战注意事项与优化
4.1 必须用 Reflect 保持默认行为
Reflect 是 ES6 配合 Proxy 推出的 API,它的方法和 Proxy 的拦截器一一对应(比如 Reflect.get 对应 get 拦截器)。在拦截器里用 Reflect 而不是直接操作 target,能确保:
- 保持对象的默认行为(比如
this 指向正确);
- 正确处理复杂场景(比如继承属性、不可写属性)。
反例(不推荐):
1 2 3 4 5
| const badHandler = { get(target, propKey) { return target[propKey]; }, };
|
正例(推荐):
1 2 3 4 5
| const goodHandler = { get(target, propKey, receiver) { return Reflect.get(target, propKey, receiver); }, };
|
4.2 避免过度代理:减少性能开销
Proxy 虽然灵活,但创建和递归代理会有性能开销,尤其是处理大型对象或频繁操作时。优化技巧:
- 避免重复代理:用
WeakMap 缓存已代理的对象,同一对象只代理一次(参考 3.1 响应式的 reactiveMap);
- 浅层代理优先:如果只需要监控顶层属性,不用递归代理嵌套对象;
- 性能关键场景不用 Proxy:比如高频更新的列表、大型数据计算,用普通对象更高效。
4.3 嵌套对象代理:需要递归处理
Proxy 只能拦截 “直接操作的属性”,如果目标对象有嵌套对象(比如 user.address.city),直接代理顶层对象无法拦截嵌套属性的操作 —— 需要在 get 拦截器里递归代理嵌套对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const nestedHandler = { get(target, propKey, receiver) { const value = Reflect.get(target, propKey, receiver); if (typeof value === 'object' && value !== null) { return new Proxy(value, nestedHandler); } return value; }, set(target, propKey, value) { console.log(`修改 ${propKey}:${value}`); return Reflect.set(target, propKey, value); }, };
const user = new Proxy({ address: { city: 'Beijing' } }, nestedHandler); user.address.city = 'Shanghai';
|
4.4 浏览器兼容性:旧环境回退
Proxy 不支持 IE 浏览器,如果需要兼容旧环境,可以用 Object.defineProperty 做回退(类似 Vue2 的响应式方案):
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
| function createCompatProxy(target, handler) { if (typeof Proxy !== 'undefined') { return new Proxy(target, handler); }
if (Array.isArray(target)) { target.forEach((_, index) => { Object.defineProperty(target, index, { get() { return handler.get?.(target, index); }, set(value) { handler.set?.(target, index, value); }, }); }); } else { Object.keys(target).forEach((key) => { Object.defineProperty(target, key, { get() { return handler.get?.(target, key); }, set(value) { handler.set?.(target, key, value); }, }); }); }
return target; }
|
五、Proxy vs Object.defineProperty:核心差异
很多人会把 Proxy 和 ES5 的 Object.defineProperty 对比,两者都是 “对象拦截” 方案,但 Proxy 更强大、更灵活:
| 特性 |
Proxy(ES6) |
Object.defineProperty(ES5) |
| 嵌套对象监听 |
✅ 支持(需递归代理) |
❌ 不支持,需手动递归实现 |
| 数组监听 |
✅ 原生支持(push、splice 等方法) |
❌ 需 hack 数组原型,不完美 |
| 新增属性监听 |
✅ 自动支持(比如 proxy.newKey = 1) |
❌ 不支持,需手动调用 defineProperty |
| 删除属性监听 |
✅ 支持(delete proxy.key) |
❌ 不支持 |
| 拦截操作数量 |
13 种(覆盖所有对象操作) |
仅 2 种(get / set) |
| 性能 |
现代浏览器优化良好,一般场景足够用 |
稍快,但功能有限 |
| 浏览器支持 |
现代浏览器(Chrome、Firefox、Edge),不支持 IE |
支持到 IE9 |
简单说:如果不需要兼容 IE,优先用 Proxy;如果需要兼容旧环境,才考虑 Object.defineProperty。
六、Proxy 的局限性
Proxy 虽强,但也有一些无法突破的限制:
- 无法拦截全等比较:
proxy === target 永远是 false(代理和原对象是两个不同的引用);
- 无法拦截某些内部方法:比如
Object.prototype.toString.call(proxy),返回的是代理对象的类型,无法自定义;
- 序列化问题:代理对象不能直接用
JSON.stringify 序列化(会序列化原对象的属性,但拦截逻辑不生效);
- 内存消耗:深度代理大型对象(比如嵌套多层的数组 / 对象),会占用较多内存。
Proxy 是 ES6 中最强大的特性之一,它的核心价值在于 “不修改原对象,却能自定义对象的行为”—— 这种 “非侵入式” 的拦截能力,让它在框架开发、工具库、数据处理等场景中大放异彩。掌握 Proxy,不仅能看懂 Vue3 等框架的底层逻辑,还能写出更灵活、更优雅的代码~