带你深入领略 Proxy 的世界
Proxy 是 es2015 标准规范加入的语法,很可能你只是听说过,但并没有用过,毕竟考虑到兼容的问题,不能轻易地使用 Proxy 特性。
但现在随着各个浏览器的更新迭代,Proxy 的支持度也越来越高:

而且使用 Proxy 进行代理和劫持,也渐渐成为了趋势。Vue3 已经用 Proxy 代替了 Object.defineProperty 实现响应式,mobx 也从 5.x 版本开始使用 Proxy 进行代理。
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 时,就会有相应的 log 输出:
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 的第 2 个参数 handler 除了可以设置 get 和 set 方法外,还有更多丰富的方法:
- get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']。
- set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。
- has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- 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
可以看到无论获取、删除还是修改数据,都可以感知到。还有数组原型链上的一些方法,如:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- 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与上面的正好相反,Proxy 是基于对象来进行代理的,因此可代理更多的类型,例如 Object, Array, Function 等;而且代理的方法也多了很多。
劣势就是兼容性不太好,即使用 polyfill,也无法完美的实现。
7. 总结
Proxy 能实现的功能还有很多,后面我们也会继续进行探索,并且尽可能去了解下基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。
欢迎您关注我的公众号:

带你深入领略 Proxy 的世界的更多相关文章
- Miox带你走进动态路由的世界——51信用卡前端团队
写在前面: 有的时候再做大型项目的时候,确实会被复杂的路由逻辑所烦恼,会经常遇到权限问题,路由跳转回退逻辑问题.这几天在网上看到了51信用卡团队开源了一个Miox,可以有效的解决这些痛点,于是乎我就做 ...
- 一文带你了解 C# DLR 的世界
一文带你了解 C# DLR 的世界 在很久之前,我写了一片文章dynamic结合匿名类型 匿名对象传参,里面我以为DLR内部是用反射实现的.因为那时候是心中想当然的认为只有反射能够在运行时解析对象的成 ...
- MYSQL(基本篇)——一篇文章带你走进MYSQL的奇妙世界
MYSQL(基本篇)--一篇文章带你走进MYSQL的奇妙世界 MYSQL算是我们程序员必不可少的一份求职工具了 无论在什么岗位,我们都可以看到应聘要求上所书写的"精通MYSQL等数据库及优化 ...
- 带你快速进入.net core的世界
[申明]:本人.NET Core小白.Linux小白.MySql小白.nginx小白.而今天要说是让你精通Linux ... 的开机与关机.nginx安装与部署.Core的Hello World .. ...
- 带你快速进入.net core的世界(转)
出处:http://www.cnblogs.com/zhaopei/p/netcore.html 阅读目录 vmware虚拟机安装 CentOS7.3安装 Windows的客户端软件 .NET Cor ...
- GDI+入门——带你走进Windows图形的世界
一.GDI+基础 1.GDI+简单介绍 GDI+是微软的新一代二维图形系统,它全然面向对象,要在Windows窗口中显示字体或绘制图形必需要使用GDI+.GDI+提供了多种画笔.画刷.图像等图形对象, ...
- 公子奇带你进入Java8流的世界(二)
在上一篇中我们带领大家简单的了解流的概念及使用场景,本节我们就来好好的介绍流的常见用法. 一.筛选和切片 对于一串流,我们有时需要取出我们需要的流中某些元素,主要是通过谓词筛选.看代码: 首先定义一个 ...
- FL Studio带你走进混音的世界
混音,是把多种音源整合到一个立体音轨或单音音轨中,通俗讲就是对多种声音进行调整后叠加在一起,这样可以让音乐听起来非常有层次感,尤其是在电音制作过程中,混音的质量更是起到了决定性的作用.音乐制作软件FL ...
- 带你开始进入NPM的世界之NPM包的开发
个人开发包的目录结构 ├── coverage //istanbul测试覆盖率生成的文件 ├── index.js //入口文件 ├── introduce.md //说明文件 ├── lib │ ...
- 带你进入 Activiti 工作流的世界
Activiti 是一个针对企业用户.开发人员 .系统管理员的轻量级工作流业务管理平台,其核心是使用 java 开发的快速 . 稳定的 BPMN2.0 流程引擎 .它可以与 spring 完美集成. ...
随机推荐
- Python查询上周五是多少号
使用Python可以轻松的查询出上周几是多少号,这周几是什么多少号,以下是查询上周五的示例: import datetime, calendar last = datetime.date.today( ...
- c# countDownEvent类
前言 把异步先总结完吧. countDownEvent 这东西是干什么的呢? 比如说我们比赛跑步,我们需要得出的是第一二三名得出后就可以先统计出来,因为比较重要,后面没有获得获奖名次的可以后续统计出来 ...
- ES6中新增的Set、Map两种数据结构
如果要用一句话来描述,我们可以说 Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构 什么是集合?什么又是字典? 集合是由一堆无序的.相关联的,且不重复的内存结构[数学中称为元素]组成的组 ...
- 学Windows批处理第一天:使用批处理命令生成一个文件并写入内容
脚本功能:1.生成一个文件,文件名格式为:yyyymmddhhmmss 2.文件中写入一段文本 操作步骤:1.新建一个文本文档(txt格式) 2.修改文件名为任意名称(我的叫create_file), ...
- maven报错:501 HTTPS Required
maven报错:501 HTTPS Required 简单来说,如果报错中出现http://repo1.maven.org/maven2/的字样的话,那么大概率就是Maven仓库的设置里的地址有问题, ...
- 力扣24(java&python)-两两交换链表中的节点(中等)
题目: 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点.你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换) 示例 1: 输入:head = [1,2,3,4] 输出:[ ...
- 【深度】阿里巴巴万级规模 K8s 集群全局高可用体系之美
简介: 台湾作家林清玄在接受记者采访的时候,如此评价自己 30 多年写作生涯:"第一个十年我才华横溢,'贼光闪现',令周边黯然失色:第二个十年,我终于'宝光现形',不再去抢风头,反而与身边的 ...
- [FE] Quasar BEX 不同位置类型的 debug 调试方式
科普:[FE] Quasar BEX 所有位置类型 types 不同类型调试,查看错误在不同的位置,如下图中的 4 个位置. Refer:https://quasar.dev/quasar-cli/d ...
- 21.3K star!推荐一款可视化自动化测试/爬虫/数据采集神器!功能免费且强大!
大家好,我是狂师! 在大数据时代,信息的获取与分析变得尤为重要.对于开发者.数据分析师乃至非技术人员来说,能够高效地采集网络数据并进行分析是一个强有力的工具.今天,我要向大家推荐的是一款功能强大.操作 ...
- dotnet 给 NuGet 包加上 Aliases 别名解决类型冲突
有时某个相同命名空间相同名字的类型被两个不同的 NuGet 包定义了,尽管这是非常少见的事情,咱需要使用到其中的一个 NuGet 包的类型,但默认情况下将会因为类型冲突而构建不通过.本文将告诉大家如何 ...