一文说透ES6 Proxy--从本质到应用场景

Last updated on December 19, 2025 am

今天是JavaScript进阶系列文章的第9篇文章了,世界就是一个巨大的草台班子,而每个班子里总是少不了诸如“黄牛”、“中间商”这样的角色,今天我们来讲讲ES6中JavaScript中的“中间商”–代理(Proxy)。

一、Proxy的本质定义:对象操作的“拦截器”

Proxy是ES6引入的元编程工具,译为“代理”,其核心功能是在目标对象与外界操作之间架设拦截层,所有对目标对象的访问必须经过该层筛选与改写。这种机制打破了对象操作的默认行为,让开发者能以极低侵入性的方式实现逻辑增强。

与传统的Object.defineProperty相比,Proxy的核心优势体现在三方面:

  • 拦截粒度更全面:可监听对象的13种基本操作(如get/set/delete/in等),而非单一属性的读写;
  • 动态性更强:无需提前声明拦截属性,新增属性自动纳入监听范围;
  • 支持更多对象类型:可代理数组、函数甚至类构造器。

所以,不难理解Vue3为什么使用Proxy来重写响应式系统,毕竟,是真的香啊。

二、 Proxy的核心写法:构造器与拦截陷阱

1. 基础语法结构

Proxy通过构造函数创建实例,语法固定为:

1
const proxy = new Proxy(target, handler);
  • target:被代理的目标对象(可是对象、数组、函数等);
  • handler:拦截规则对象,包含各类“陷阱函数”(Trap),未定义的陷阱将默认透传操作至目标对象。

2. 常用拦截陷阱实战

(1)属性读写拦截(get/set)

最常用的陷阱组合,可实现数据校验、默认值填充等功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const userValidator = {
set(target, prop, value, receiver) {
// 年龄必须为0-120的数字
if (prop === 'age' && (!Number.isInteger(value) || value < 0 || value > 120)) {
throw new Error(`无效年龄:${value}`);
}
// 使用Reflect确保this指向正确
return Reflect.set(target, prop, value, receiver);
},
get(target, prop, receiver) {
// 不存在的属性返回默认值
return prop in target ? Reflect.get(target, prop, receiver) : '未设置';
}
};

const user = new Proxy({}, userValidator);
user.name = 'Alice'; // 正常设置
user.age = 130; // 抛出Error: 无效年龄
console.log(user.gender); // 输出"未设置"

(2)函数调用拦截(apply)

专门用于代理函数,可实现调用日志、参数校验等:

1
2
3
4
5
6
7
8
9
10
11
12
13
const calcProxy = new Proxy((a, b) => a + b, {
apply(target, thisArg, args) {
console.log(`执行加法:${args.join('+')}`);
// 校验参数类型
if (!args.every(Number.isFinite)) {
throw new Error('参数必须为数字');
}
return target.apply(thisArg, args);
}
});

calcProxy(2, 3); // 输出"执行加法:2+3",返回5
calcProxy(2, '3'); // 抛出Error

(3)构造器拦截(construct)

拦截new操作,可控制实例创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const UserProxy = new Proxy(class {
constructor(name) {
this.name = name;
}
}, {
construct(target, args) {
console.log(`创建用户:${args[0]}`);
// 强制添加默认属性
const instance = new target(...args);
instance.createTime = Date.now();
return instance;
}
});

const user = new UserProxy('Bob');
// 输出"创建用户:Bob",user包含name与createTime属性

三、Proxy的典型使用场景

数据校验与格式化

通过set陷阱实现强类型校验,广泛用于表单处理、配置项验证等场景。例如限制商品价格必须为正数:

1
2
3
4
5
6
7
8
9
10
11
12
const priceHandler = {
set(target, prop, value) {
if (prop.includes('price') && (typeof value !== 'number' || value <= 0)) {
throw new Error(`无效${prop}${value}`);
}
Reflect.set(target, prop, value);
}
};

const product = new Proxy({ name: '手机' }, priceHandler);
product.salePrice = 3999; // 正常
product.salePrice = -100; // 抛出错误

响应式编程基石

Vue 3的响应式系统核心基于Proxy实现,通过拦截属性读写触发依赖收集与视图更新。简化原理如下:

1
2
3
4
5
6
7
8
9
10
11
12
function reactive(target) {
return new Proxy(target, {
get(target, prop) {
track(target, prop); // 收集依赖
return Reflect.get(target, prop);
},
set(target, prop, value) {
Reflect.set(target, prop, value);
trigger(target, prop); // 触发更新
}
});
}

相较于Vue 2的Object.defineProperty,此实现天然支持数组索引修改、新增属性等场景。

日志与调试增强

通过拦截操作自动记录日志,无需侵入业务代码。例如监控对象操作轨迹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createLogger(target, name) {
return new Proxy(target, {
get: logOperation('读取'),
set: logOperation('修改'),
deleteProperty: logOperation('删除')
});

function logOperation(type) {
return (target, prop) => {
console.log(`[${name}]${type}属性${prop}`);
return Reflect[type === '删除' ? 'deleteProperty' : type](target, prop);
};
}
}

const user = createLogger({ name: 'Alice' }, '用户数据');
user.age = 25; // 输出"[用户数据]修改属性age"
delete user.age; // 输出"[用户数据]删除属性age"

虚拟化与权限控制

创建“虚拟对象”隐藏真实数据结构,同时实现权限校验。例如限制私有属性访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createSecureObject(data) {
const privateKeys = new Set(['_password']);
return new Proxy(data, {
get(target, prop) {
if (privateKeys.has(prop)) {
throw new Error('无权访问私有属性');
}
// 虚拟化处理:名称转为大写
return prop === 'name' ? target[prop].toUpperCase() : target[prop];
}
});
}

const user = createSecureObject({ name: 'bob', _password: '123' });
console.log(user.name); // 输出"BOB"
console.log(user._password); // 抛出错误

四、使用注意事项

虽然Proxy在诸多场景中表现很出色,但依然存在着一些需要再使用中注意的问题:

  • 兼容性限制:不支持IE浏览器,现代浏览器需考虑降级方案(如Vue 3的IE兼容版仍用Object.defineProperty,哈哈,你们就偷着乐吧);
  • this指向问题:陷阱函数中的this指向handler而非目标对象,需用Reflect确保上下文正确;
  • 性能权衡:代理层会带来轻微性能损耗,高频操作场景需评估是否使用,不过这种损耗在现代计算机性能前可以忽略不计;
  • 不可代理场景:某些原生对象的内部属性(如Date[[PrimitiveValue]])无法拦截,这个属于冷知识了。

【往期精彩】