本文深入解析 JavaScript 中的 Proxy 对象,涵盖其基本结构、13种拦截方法详解,以及与 Reflect 对象的配合使用。通过数组代理、函数调用统计、防抖实现及观察者模式等实战案例,展示 Proxy 在现代前端框架(如 Vue3、Mobx)中的核心作用,并对比其与 Object.defineProperty 的优劣。
1. Proxy 的基础构造
Proxy 的核心用法如下:
/**
* target: 表示要代理的目标,可以是object, array, function类型
* handler: 是一个对象,可以编写各种代理的方法
*/
const proxy = new Proxy(target, handler);
假设我们需要对某个对象进行代理,可以通过定义 get 和 set 方法来拦截属性的读取与赋值行为:
const person = {
name: 'wenzi',
age: 20,
};
const personProxy = new Proxy(person, {
get(target, key, receiver) {
console.log(`get value by ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`set ${key}, old value ${target[key]} to ${value}`);
target[key] = value;
},
});
Proxy 本质上仅作为代理层存在,personProxy 继承了 person 的所有属性和方法。当通过 personProxy 访问或修改 name 属性时,会触发相应的日志记录:
personProxy.name; // "wenzi"
// log: get value by name
personProxy.name = 'hello';
// log: set name, old value wenzi to hello
同时,通过 personProxy 修改数据时,底层原对象的数据也会同步更新。打印 person 对象后,可以看到 name 字段的值已更新为 hello:
console.log(person); // {name: "hello", age: 20}
Proxy 的第二个参数 handler 除了支持 get 和 set 外,还包含多种其他拦截方法:
1.get(target, propKey, receiver):拦截对象属性的读取操作,例如 proxy.foo 或 proxy['foo']。
2.set(target, propKey, value, receiver):拦截对象属性的赋值操作,例如 proxy.foo = v 或 proxy['foo'] = v,该方法返回布尔值。
3.has(target, propKey):拦截 propKey in proxy 的检查操作,返回布尔值。
4.deleteProperty(target, propKey):拦截 delete proxy[propKey] 的删除操作,返回布尔值。
5.ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) 以及 for...in 循环,返回数组。此方法返回目标对象所有自身属性的名称,而 Object.keys() 仅返回可遍历的自身属性。
6.getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
7.defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc) 和 Object.defineProperties(proxy, propDescs),返回布尔值。
8.preventExtensions(target):拦截 Object.preventExtensions(proxy),返回布尔值。
9.getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回对象。
10.isExtensible(target):拦截 Object.isExtensible(proxy),返回布尔值。
11.setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回布尔值。若目标对象为函数,则额外支持两种拦截操作。
12.apply(target, object, args):拦截 Proxy 实例作为函数调用的行为,例如 proxy(...args)、proxy.call(object, ...args) 或 proxy.apply(...)。
13.construct(target, args):拦截 Proxy 实例作为构造函数调用的行为,例如 new proxy(...args)。
例如,当我们使用 delete 删除元素时,可以通过 deleteProperty() 方法来拦截该操作。沿用上述代理 person 的代码,我们添加 deleteProperty 方法:
const person = {
name: 'wenzi',
age: 20,
};
const personProxy = new Proxy(person, {
// 忽略get和set方法,与上面一样
// ...
deleteProperty(target, key, receiver) {
console.log(`delete key ${key}`);
delete target[key];
},
});
当执行 delete 操作时:
delete personProxy['age'];
// log: delete key age
2. Proxy 与 Reflect 的关系
Proxy 与 Reflect 紧密相关,Reflect 中的方法及其使用方式与 Proxy 完全一致。
例如,上述 Proxy 中的 get()、set() 和 deleteProperty() 方法原本直接操作原代理对象,这里我们改为使用Reflect来执行:
const personProxy = new Proxy(person, {
get(target, key, receiver) {
console.log(`get value by ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`set ${key}, old value ${target[key]} to ${value}`);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key, receiver) {
console.log(`delete key ${key}`);
return Reflect.deleteProperty(target, key, receiver);
},
});
可以发现,这样完美实现了上述功能。

3. 代理数组
在之前的文章《Vue 中对数组特殊的操作》中,我们讨论过 Vue 为何未使用 Object.defineProperty 来劫持数据,而是重写了 Array 原型链上的几个方法,通过重写的方法来实现 Vue 模板中数据的更新。
但如果使用 Proxy,则可以直接代理数组:
const arr = [1, 2, 3, 4];
const arrProxy = new Proxy(arr, {
get(target, key, receiver) {
console.log('arrProxy.get', target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('arrProxy.set', target, key, value);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log('arrProxy.deleteProperty', target, key);
return Reflect.deleteProperty(target, key);
},
});
现在我们来操作一下代理后的数组 arrProxy 进行观察:
arrProxy[2] = 22; // arrProxy.set (4) [1, 2, 3, 4] 2 22
arrProxy[3]; // arrProxy.get (4) [1, 2, 22, 4] 3
delete arrProxy[2]; // arrProxy.deleteProperty (4) [1, 2, 22, 4] 2
arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作
arrProxy.length; // arrProxy.get (5) [1, 2, empty, 4, 5] length
可以看到,无论是获取、删除还是修改数据,Proxy 都能感知到。此外,数组原型链上的部分方法,如:
1.push()
2.pop()
3.shift()
4.unshift()
5.splice()
6.sort()
7.reverse()
也都可以通过 Proxy 中的代理方法进行劫持。
concat() 方法较为特殊,它属于赋值操作且不改变原数组,因此在调用 concat() 方法操作数组时,若无赋值操作,则仅触发 get() 拦截。

4. 代理函数
Proxy 中还包含 apply() 方法,用于拦截函数作为函数调用时的操作。
const getSum = (...args) => {
if (!args.every((item) => typeof item === 'number')) {
throw new TypeError('参数应当均为number类型');
}
return args.reduce((sum, item) => sum + item, 0);
};
const fnProxy = new Proxy(getSum, {
/**
* @params {Fuction} target 代理的对象
* @params {any} ctx 执行的上下文
* @params {any} args 参数
*/
apply(target, ctx, args) {
console.log('ctx', ctx);
console.log(`execute fn ${getSum.name}, args: ${args}`);
return Reflect.apply(target, ctx, args);
},
});
执行 fnProxy:
// 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4
fnProxy(1, 2, 3, 4);
// ctx为undefined, Uncaught TypeError: 参数应当均为number类型
fnProxy(1, 2, 3, '4');
// 10, ctx为window, log: execute fn getSum, args: 1,2,3,4
fnProxy.apply(window, [1, 2, 3, 4]);
// 6, ctx为window, log: execute fn getSum, args: 1,2,3
fnProxy.call(window, 1, 2, 3);
// 6, ctx为person, log: execute fn getSum, args: 1,2,3
fnProxy.apply(person, [1, 2, 3]);
5. 一些简单的应用场景
我们知道 Vue3 已使用 Proxy 重写了响应式系统,mobx 也采用了 Proxy 模式。在可见的未来,Proxy 的应用场景将更加广泛,此处我们简要介绍几个案例。
5.1 统计函数被调用的上下文和次数
此处我们利用 Proxy 代理函数,以记录函数被调用时的上下文及调用次数。
const countExecute = (fn) => {
let count = 0;
return new Proxy(fn, {
apply(target, ctx, args) {
++count;
console.log('ctx上下文:', ctx);
console.log(`${fn.name} 已被调用 ${count} 次`);
return Reflect.apply(target, ctx, args);
},
});
};
现在我们来代理刚才的 getSum() 方法:
const getSum = (...args) => {
if (!args.every((item) => typeof item === 'number')) {
throw new TypeError('参数应当均为number类型');
}
return args.reduce((sum, item) => sum + item, 0);
};
const useSum = countExecute(getSum);
useSum(1, 2, 3); // getSum 已被调用 1 次
useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次
useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次
5.2 实现一个防抖功能
基于上述统计函数调用次数的功能,这也为我们实现函数防抖提供了灵感。
const throttleByProxy = (fn, rate) => {
let lastTime = 0;
return new Proxy(fn, {
apply(target, ctx, args) {
const now = Date.now();
if (now - lastTime > rate) {
lastTime = now;
return Reflect.apply(target, ctx, args);
}
},
});
};
const logTimeStamp = () => console.log(Date.now());
window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300));
logTimeStamp() 至少需要间隔 300ms 才能执行一次。
5.3 实现观察者模式
在此处,我们实现一个最简单的类似 mobx 的观察者模式。
const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
return new Proxy(obj, {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
list.forEach((observer) => observer());
return result;
},
});
};
const person = observable({ name: 'wenzi', age: 20 });
const App = () => {
console.log(`App -> name: ${person.name}, age: ${person.age}`);
};
observe(App);
person 是使用 Proxy 创建的代理对象,每当 person 中的属性发生变化时,便会执行 App() 函数。由此实现了一个简单的响应式状态管理。
6. Proxy 与 Object.defineProperty 的对比
上述许多示例使用 Object.defineProperty 同样可以实现。那么这两者各自有哪些优缺点呢?
6.1 Object.defineProperty 的优劣
Object.defineProperty 的兼容性相比 Proxy 更为优秀,除极老旧的 IE6、IE7 浏览器外,其他主流浏览器均提供支持。
但 Object.defineProperty 支持的方法众多,且主要基于属性进行拦截。因此在 Vue2 中,只能通过重写 Array 原型链上的方法来操作数组。
6.2 Proxy 的优劣
Proxy 与上述情况正好相反,它是基于对象进行代理的,因此可代理更多类型,例如 Object、Array、Function 等;且支持代理的方法也更多。
其劣势在于兼容性较差,即使使用 polyfill 也无法完美实现。
7. 总结
Proxy 能实现的功能还有很多,后续我们将继续探索,并尽可能深入了解基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。
以上就是《JS带你深入领略Proxy的世界》的详细内容,更多关于JS中的代理Proxy的资料请关注本站其它相关文章!