代理(Proxy):ES6 元编程的 “对象拦截器”

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 = {
// 拦截“读属性”操作(比如 proxy.name)
get(target, propKey) {
console.log(`正在读取属性:${propKey}`);
// 用 Reflect.get 保持默认读逻辑(避免破坏原对象行为)
return Reflect.get(target, propKey);
},
// 拦截“写属性”操作(比如 proxy.age = 29)
set(target, propKey, value) {
console.log(`正在修改属性 ${propKey}:从 ${target[propKey]} 改成 ${value}`);
// 用 Reflect.set 保持默认写逻辑
return Reflect.set(target, propKey, value);
},
};

// 创建代理对象
const proxyUser = new Proxy(user, userHandler);

// 测试拦截效果
console.log(proxyUser.name); // 触发 get:输出“正在读取属性:name”,再输出“Alice”
proxyUser.age = 29; // 触发 set:输出“正在修改属性 age:从 28 改成 29”

二、13 种核心拦截方法:覆盖所有对象操作

Proxy 提供了 13 种拦截方法,覆盖了对象的几乎所有基础操作。我整理了日常开发中最常用的几种,按使用频率排序:

拦截器方法 触发时机 示例 核心作用
get(target, propKey, receiver) 读取对象属性时 proxy.nameproxy[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
// 存储依赖:key 是目标对象,value 是该对象各属性的依赖列表
const reactiveMap = new WeakMap();

// 收集依赖:记录“哪个函数在用这个属性”
function track(target, propKey) {
// 这里简化处理,实际 Vue 中会关联组件渲染函数
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);
// 嵌套对象递归代理(比如 user.address.city 也能响应)
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'; // 触发 set → 输出“属性 name 变了,更新视图!”
user.address.city = 'Shanghai'; // 嵌套对象也触发 → 输出“属性 city 变了,更新视图!”

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':
// 验证 age 必须是正整数
if (!Number.isInteger(value) || value < 0) {
throw new TypeError(`年龄 ${value} 无效!必须是正整数`);
}
break;
case 'name':
// 验证 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'; // 报错:TypeError: 年龄 thirty 无效!必须是正整数
person.name = ''; // 报错:TypeError: 姓名不能为空!

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
// 目标对象:存储 API 基础配置
const apiConfig = { baseURL: 'https://api.example.com' };

// API 拦截器:拦截属性访问,返回封装后的请求函数
const apiHandler = {
get(target, endpoint) {
// 比如访问 api.users,返回一个请求 /users 接口的函数
return async (params) => {
// 统一加请求日志
console.log(`调用 API:${endpoint},参数:`, params);
// 统一拼接基础 URL
const url = `${target.baseURL}/${endpoint}`;
// 发送请求
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
return response.json();
};
},
};

// 创建 API 代理
const api = new Proxy(apiConfig, apiHandler);

// 调用 API:简洁且统一
const userData = await api.users({ id: 123 }); // 调用 https://api.example.com/users
const orderData = await api.orders({ userId: 123 }); // 调用 https://api.example.com/orders

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) {
// 从 localStorage 读取已有数据(没有则用初始值)
const storedData = localStorage.getItem(storageKey);
const state = storedData ? JSON.parse(storedData) : initialState;

// 拦截 set 操作:修改后自动保存
const handler = {
set(target, propKey, value) {
const success = Reflect.set(target, propKey, value);
// 自动同步到 localStorage
localStorage.setItem(storageKey, JSON.stringify(target));
return success;
},
};

return new Proxy(state, handler);
}

// 创建自动持久化的配置对象
const appSettings = createPersistentState('app-settings', {
theme: 'light',
fontSize: 16,
});

// 测试:修改后自动保存到 localStorage
appSettings.theme = 'dark'; // localStorage 里的 app-settings 会自动更新
appSettings.fontSize = 18; // 同样自动保存

3.5 数组变化监听:完美支持数组方法

Proxy 能原生拦截数组的 pushpopsplice 等方法,不用像 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) {
// 拦截数组的变异方法(push、pop 等)
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); // 触发拦截:输出“数组执行 push,参数:[4]”,数组变成 [1,2,3,4]
list.splice(0, 1); // 触发拦截:输出“数组执行 splice,参数:[0,1]”,数组变成 [2,3,4]

四、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]; // 直接操作 target,可能破坏继承等默认行为
},
};

正例(推荐):

1
2
3
4
5
const goodHandler = {
get(target, propKey, receiver) {
return Reflect.get(target, propKey, receiver); // 用 Reflect 保持默认行为
},
};

4.2 避免过度代理:减少性能开销

Proxy 虽然灵活,但创建和递归代理会有性能开销,尤其是处理大型对象或频繁操作时。优化技巧:

  1. 避免重复代理:用 WeakMap 缓存已代理的对象,同一对象只代理一次(参考 3.1 响应式的 reactiveMap);
  2. 浅层代理优先:如果只需要监控顶层属性,不用递归代理嵌套对象;
  3. 性能关键场景不用 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);
// 如果值是对象且非 null,递归代理
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'; // 触发 set:输出“修改 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) {
// 现代浏览器:用 Proxy
if (typeof Proxy !== 'undefined') {
return new Proxy(target, handler);
}

// 旧浏览器:用 Object.defineProperty 模拟(仅支持 get/set)
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 虽强,但也有一些无法突破的限制:

  1. 无法拦截全等比较proxy === target 永远是 false(代理和原对象是两个不同的引用);
  2. 无法拦截某些内部方法:比如 Object.prototype.toString.call(proxy),返回的是代理对象的类型,无法自定义;
  3. 序列化问题:代理对象不能直接用 JSON.stringify 序列化(会序列化原对象的属性,但拦截逻辑不生效);
  4. 内存消耗:深度代理大型对象(比如嵌套多层的数组 / 对象),会占用较多内存。

Proxy 是 ES6 中最强大的特性之一,它的核心价值在于 “不修改原对象,却能自定义对象的行为”—— 这种 “非侵入式” 的拦截能力,让它在框架开发、工具库、数据处理等场景中大放异彩。掌握 Proxy,不仅能看懂 Vue3 等框架的底层逻辑,还能写出更灵活、更优雅的代码~