本文转载自微信公众号「全栈修仙之路」,作者阿宝哥。转载本文请联系全栈修仙之路公众号。

阿宝哥将从 6 个方面入手,带你一步一步揭开 Proxy 对象的神秘面纱。阅读完本文,你将了解以下内容:
一、聊一聊代理
在日常工作中,相信挺多小伙伴都用过 Web 调试代理工具,比如 Fiddler 或 Charles。通过使用 Web 调试代理工具,我们可以抓取 HTTP/HTTPS 协议请求,还可以手动修改请求参数和响应结果。不仅如此,在调试线上问题时,利用 Web 调试代理工具,你还可以把线上压缩混淆过 的 JS 文件映射成本地 未压缩混淆过 的 JS 文件。
在简单介绍了 Web 调试代理工具的基本功能之后,我们来看一下使用 Web 调试代理工具的 HTTP 请求流程:
通过上图可知,在引入 Web 调试代理工具之后,我们发起的 HTTP 请求都会通过 Web Proxy 进行转发和处理。增加了 Web Proxy 代理层,让我们能够更好地控制 HTTP 请求的流程。对于单页应用程序来说,当从服务器获取数据之后,我们就会读取相应的数据在页面上显示出来:
以上流程与浏览器直接从服务器获取数据类似:
为了能够灵活控制 HTTP 请求的流程,我们增加了的 Web Proxy 层。那么我们能否控制数据对象的读取流程呢?答案是可以的,我们可以利用 Web API,比如 Object.defineProperty 或 Proxy API。在引入 Web API 之后,数据的访问流程如下图所示:
接下来,阿宝哥将重点介绍 Proxy API,它可是 Vue3 实现数据响应式幕后的 “功臣” 哟。对它感兴趣的小伙伴,跟阿宝哥一起学起来吧。
二、Proxy 对象简介
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy 的构造函数语法为:
- const p = new Proxy(target, handler)
 
相关的参数说明如下:
在介绍 Proxy 对象的使用示例前,我们先来了解一下它的兼容性:
(图片来源:https://caniuse.com/?search=Proxy)
由上图可知,Proxy API 的兼容性并不是很好,所以大家在使用的时候要注意其兼容性问题。
2.1 Proxy 对象使用示例
了解完 Proxy 构造函数,我们来看一个简单的例子:
- const man = {
 - name: "阿宝哥",
 - };
 - const proxy = new Proxy(man, {
 - get(target, property, receiver) {
 - console.log(`正在访问${property}属性`);
 - return target[property];
 - },
 - });
 - console.log(proxy.name);
 - console.log(proxy.age);
 
在以上示例中,我们使用了 Proxy 构造函数为 man 对象,创建了一个代理对象。在创建代理对象时,我们定义了一个 get 捕获器,用于捕获属性读取的操作。 捕获器的作用就是用于拦截用户对目标对象的相关操作,在这些操作传播到目标对象之前,会先调用对应的捕获器函数,从而拦截并修改相应的行为。
在设置了 get 捕获器之后,当成功运行以上的示例代码,控制台会输出以下结果:
- 正在访问name属性
 - 阿宝哥
 - 正在访问age属性
 - undefined
 
通过观察以上输出结果,我们可以发现 get 捕获器 不仅可以拦截已知属性的读取操作,也可以拦截未知属性的读取操作。在创建 Proxy 对象时,除了定义 get 捕获器 之外,我们还可以定义其他的捕获器,比如 has、set、delete、apply 或 ownKeys 等。
2.2 handler 对象支持的捕获器
handler 对象支持 13 种捕获器,这里阿宝哥只列举以下 5 种常用的捕获器:
需要注意的是,所有的捕获器是可选的。如果没有定义某个捕获器,那么就会保留源对象的默认行为。 看完上面的捕获器介绍,是不是觉得 Proxy 对象很强大。
三、Reflect 对象简介
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
在介绍 Reflect 对象的使用示例前,我们先来了解一下它的兼容性:
(图片来源:https://caniuse.com/?search=Reflect)
3.1 Reflect 对象使用示例
- const man = {
 - name: "阿宝哥",
 - city: "Xiamen",
 - };
 - console.log(Reflect.set(man, "sex", 1)); // true
 - console.log(Reflect.has(man, "name")); // true
 - console.log(Reflect.has(man, "age")); // false
 - console.log(Reflect.ownKeys(man)); // [ 'name', 'city', 'sex' ]
 
除了示例中介绍的 set、has 和 ownKeys 方法之外,Reflect 对象还支持 get、defineProperty 和 deleteProperty 等方法。下面阿宝哥将简单介绍 Reflect 对象所支持的一些静态方法。
3.2 Reflect 对象支持的静态方法
Reflect 的所有属性和方法都是静态的,该对象提供了与 Proxy handler 对象相关的 13 个方法。同样,这里阿宝哥只列举以下 5 个常用的方法:
在实际的 Proxy 使用场景中,我们往往会结合 Reflect 对象提供的静态方法来实现某些特定的功能。为了让大家能够更好地理解并掌握 Proxy 对象,接下来的环节,阿宝哥将列举 Proxy 对象的 6 个使用场景。
四、Proxy 使用场景
这里我们先来介绍 Proxy 对象的第一个使用场景 —— 增强型数组。
4.1 增强型数组
定义 enhancedArray 函数
- function enhancedArray(arr) {
 - return new Proxy(arr, {
 - get(target, property, receiver) {
 - const range = getRange(property);
 - const indices = range ? range : getIndices(property);
 - const values = indices.map(function (index) {
 - const key = index < 0 ? String(target.length + index) : index;
 - return Reflect.get(target, key, receiver);
 - });
 - return values.length === 1 ? values[0] : values;
 - },
 - });
 - function getRange(str) {
 - var [start, end] = str.split(":").map(Number);
 - if (typeof end === "undefined") return false;
 - let range = [];
 - for (let i = start; i < end; i++) {
 - range = range.concat(i);
 - }
 - return range;
 - }
 - function getIndices(str) {
 - return str.split(",").map(Number);
 - }
 - }
 
使用 enhancedArray 函数
- const arr = enhancedArray([1, 2, 3, 4, 5]);
 - console.log(arr[-1]); //=> 5
 - console.log(arr[[2, 4]]); //=> [ 3, 5 ]
 - console.log(arr[[2, -2, 1]]); //=> [ 3, 4, 2 ]
 - console.log(arr["2:4"]); //=> [ 3, 4]
 - console.log(arr["-2:3"]); //=> [ 4, 5, 1, 2, 3 ]
 
由以上的输出结果可知,增强后的数组对象,就可以支持负数索引、分片索引等功能。除了可以增强数组之外,我们也可以使用 Proxy API 来增强普通对象。
4.2 增强型对象
创建 enhancedObject 函数
- const enhancedObject = (target) =>
 - new Proxy(target, {
 - get(target, property) {
 - if (property in target) {
 - return target[property];
 - } else {
 - return searchFor(property, target);
 - }
 - },
 - });
 - let value = null;
 - function searchFor(property, target) {
 - for (const key of Object.keys(target)) {
 - if (typeof target[key] === "object") {
 - searchFor(property, target[key]);
 - } else if (typeof target[property] !== "undefined") {
 - value = target[property];
 - break;
 - }
 - }
 - return value;
 - }
 
使用 enhancedObject 函数
- const data = enhancedObject({
 - user: {
 - name: "阿宝哥",
 - settings: {
 - theme: "dark",
 - },
 - },
 - });
 - console.log(data.user.settings.theme); // dark
 - console.log(data.theme); // dark
 
以上代码运行后,控制台会输出以下代码:
- dark
 - dark
 
通过观察以上的输出结果可知,使用 enhancedObject 函数处理过的对象,我们就可以方便地访问普通对象内部的深层属性。
4.3 创建只读的对象
创建 Proxy 对象
- const man = {
 - name: "semlinker",
 - };
 - const handler = {
 - set: "Read-Only",
 - defineProperty: "Read-Only",
 - deleteProperty: "Read-Only",
 - preventExtensions: "Read-Only",
 - setPrototypeOf: "Read-Only",
 - };
 - const proxy = new Proxy(man, handler);
 
使用 proxy 对象
- console.log(proxy.name);
 - proxy.name = "kakuqo";
 
以上代码运行后,控制台会输出以下代码:
- semlinker
 - proxy.name = "kakuqo";
 - ^
 - TypeError: 'Read-Only' returned for property 'set' of object '#
 
观察以上的异常信息可知,导致异常的原因是因为 handler 对象的 set 属性值不是一个函数。如果不希望抛出运行时异常,我们可以定义一个 freeze 函数:
- function freeze (obj) {
 - return new Proxy(obj, {
 - set () { return true; },
 - deleteProperty () { return false; },
 - defineProperty () { return true; },
 - setPrototypeOf () { return true; }
 - });
 - }
 
定义好 freeze 函数,我们使用数组对象来测试一下它的功能:
- let frozen = freeze([1, 2, 3]);
 - frozen[0] = 6;
 - delete frozen[0];
 - frozen = Object.defineProperty(frozen, 0, { value: 66 });
 - console.log(frozen); // [ 1, 2, 3 ]
 
上述代码成功执行后,控制台会输出 [ 1, 2, 3 ],很明显经过 freeze 函数处理过的数组对象,已经被 “冻结” 了。
4.4 拦截方法调用
定义 traceMethodCalls 函数
- function traceMethodCalls(obj) {
 - const handler = {
 - get(target, propKey, receiver) {
 - const origMethod = target[propKey]; // 获取原始方法
 - return function (...args) {
 - const result = origMethod.apply(this, args);
 - console.log(
 - propKey + JSON.stringify(args) + " -> " + JSON.stringify(result)
 - );
 - return result;
 - };
 - },
 - };
 - return new Proxy(obj, handler);
 - }
 
使用 traceMethodCalls 函数
- const obj = {
 - multiply(x, y) {
 - return x * y;
 - },
 - };
 - const tracedObj = traceMethodCalls(obj);
 - tracedObj.multiply(2, 5); // multiply[2,5] -> 10
 
上述代码成功执行后,控制台会输出 multiply[2,5] -> 10,即我们能够成功跟踪 obj对象中方法的调用过程。其实,除了能够跟踪方法的调用,我们也可以跟踪对象中属性的访问,具体示例如下:
- function tracePropAccess(obj, propKeys) {
 - const propKeySet = new Set(propKeys);
 - return new Proxy(obj, {
 - get(target, propKey, receiver) {
 - if (propKeySet.has(propKey)) {
 - console.log("GET " + propKey);
 - }
 - return Reflect.get(target, propKey, receiver);
 - },
 - set(target, propKey, value, receiver) {
 - if (propKeySet.has(propKey)) {
 - console.log("SET " + propKey + "=" + value);
 - }
 - return Reflect.set(target, propKey, value, receiver);
 - },
 - });
 - }
 - const man = {
 - name: "semlinker",
 - };
 - const tracedMan = tracePropAccess(man, ["name"]);
 - console.log(tracedMan.name); // GET name; semlinker
 - console.log(tracedMan.age); // undefined
 - tracedMan.name = "kakuqo"; // SET name=kakuqo
 
在以上示例中,我们定义了一个 tracePropAccess 函数,该函数接收两个参数:obj 和 propKeys,它们分别表示需跟踪的目标和需跟踪的属性列表。调用 tracePropAccess 函数后,会返回一个代理对象,当我们访问被跟踪的属性时,控制台就会输出相应的访问日志。
4.5 隐藏属性
创建 hideProperty 函数
- const hideProperty = (target, prefix = "_") =>
 - new Proxy(target, {
 - has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
 - ownKeys: (obj) =>
 - Reflect.ownKeys(obj).filter(
 - (prop) => typeof prop !== "string" || !prop.startsWith(prefix)
 - ),
 - get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined),
 - });
 
使用 hideProperty 函数
- const man = hideProperty({
 - name: "阿宝哥",
 - _pwd: "www.semlinker.com",
 - });
 - console.log(man._pwd); // undefined
 - console.log('_pwd' in man); // false
 - console.log(Object.keys(man)); // [ 'name' ]
 
通过观察以上的输出结果,我们可以知道,利用 Proxy API,我们实现了指定前缀属性的隐藏。除了能实现隐藏属性之外,利用 Proxy API,我们还可以实现验证属性值的功能。
4.6 验证属性值
创建 validatedUser 函数
- const validatedUser = (target) =>
 - new Proxy(target, {
 - set(target, property, value) {
 - switch (property) {
 - case "email":
 - const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 - if (!regex.test(value)) {
 - console.error("The user must have a valid email");
 - return false;
 - }
 - break;
 - case "age":
 - if (value < 20 || value > 80) {
 - console.error("A user's age must be between 20 and 80");
 - return false;
 - }
 - break;
 - }
 - return Reflect.set(...arguments);
 - },
 - });
 
使用 validatedUser 函数
- let user = {
 - email: "",
 - age: 0,
 - };
 - user = validatedUser(user);
 - user.email = "semlinker.com"; // The user must have a valid email
 - user.age = 100; // A user's age must be between 20 and 80
 
上述代码成功执行后,控制台会输出以下结果:
- The user must have a valid email
 - A user's age must be between 20 and 80
 
介绍完 Proxy 对象的使用场景之后,我们来继续介绍与 Proxy 对象相关的一些问题。
五、Proxy 相关问题
5.1 this 的指向问题
- const target = {
 - foo() {
 - return {
 - thisIsTarget: this === target,
 - thisIsProxy: this === proxy,
 - };
 - },
 - };
 - const handler = {};
 - const proxy = new Proxy(target, handler);
 - console.log(target.foo()); // { thisIsTarget: true, thisIsProxy: false }
 - console.log(proxy.foo()); // { thisIsTarget: false, thisIsProxy: true }
 
上述代码成功执行后,控制台会输出以下结果:
- { thisIsTarget: true, thisIsProxy: false }
 - { thisIsTarget: false, thisIsProxy: true }
 
通过以上输出的结果,foo 方法中的 this 指向与当前的调用者有关。看起来挺简单的,但在一些场景下如果稍不注意的话,就会出现问题,比如以下这个示例:
- const _name = new WeakMap();
 - class Person {
 - constructor(name) {
 - _name.set(this, name);
 - }
 - get name() {
 - return _name.get(this);
 - }
 - }
 
在以上示例中,我们使用 WeakMap 对象来存储 Person 对象的私有信息。定义完 Person类,我们就可以通过以下方式来使用它:
- const man = new Person("阿宝哥");
 - console.log(man.name); // 阿宝哥
 - const proxy = new Proxy(man, {});
 - console.log(proxy.name); // undefined
 
对于以上的代码,当我们通过 proxy 对象来访问 name 属性时,你会发现输出的结果是undefined。这是因为当使用 proxy.name 的方式访问 name 属性时,this 指向的是 proxy 对象,而 _name WeakMap 对象中存储的是 man 对象,所以输出的结果是 undefined。
然而,对于以上的问题,如果我们按照以下方式定义 Person 类,就不会出现以上问题:
- class Person {
 - constructor(name) {
 - this._name = name;
 - }
 - get name() {
 - return this._name;
 - }
 - }
 - const man = new Person("阿宝哥");
 - console.log(man.name); // 阿宝哥
 - const proxy = new Proxy(man, {});
 - console.log(proxy.name); // 阿宝哥
 
另外,如果你对 WeakMap 感兴趣的话,可以阅读 你不知道的 WeakMap 这篇文章。
5.2 get 捕获器 receiver 参数是什么
- const p = new Proxy(target, {
 - get: function(target, property, receiver) {
 - // receiver
 - }
 - });
 
get 捕获器用于拦截对象的读取属性操作,该捕获器含有三个参数:
为了更好地了解 receiver 参数的描述信息,我们来举个具体的示例:
- const proxy = new Proxy({},
 - {
 - get: function (target, property, receiver) {
 - return receiver;
 - },
 - }
 - );
 - console.dir(proxy.getReceiver === proxy); // true
 - var inherits = Object.create(proxy);
 - console.dir(inherits.getReceiver === inherits); // true
 
那么我们能否改变 receiver 指向的对象呢?答案是可以的,通过 Reflect 对象提供的get 方法,我们可以动态设置 receiver 对象的值,具体使用方式如下所示:
- console.dir(Reflect.get(proxy, "getReceiver", "阿宝哥"));
 
其实 receiver 的名称是来源于 ECMAScript 规范:
以上的 [[Get]] 和 [[Set]] 被称为内部方法,ECMAScript 引擎中的每个对象都与一组内部方法相关联,这些内部方法定义了其运行时行为。
需要注意的是,这些内部方法不是 ECMAScript 语言的一部分。对于对象的访问器属性来说,在执行内部代码时,Receiver 将被作为 this 的值,同样使用 Reflect 对象提供的 API,我们也可以通过设置 receiver 参数的值来改变 this 的值:
- const obj = {
 - get foo() {
 - return this.bar;
 - },
 - };
 - console.log(Reflect.get(obj, "foo")); // undefined
 - console.log(Reflect.get(obj, "foo", { bar: 2021 })); // 2021
 
5.3 包装内置构造函数的实例
当使用 Proxy 包装内置构造函数实例的时候,可能会出现一些问题。比如使用 Proxy 代理 Date 构造函数的实例:
- const target = new Date();
 - const handler = {};
 - const proxy = new Proxy(target, handler);
 - proxy.getDate(); // Error
 
当以上代码运行后,控制台会输出以下异常信息:
- proxy.getDate();
 - ^
 - TypeError: this is not a Date object.
 
出现以上问题的原因是因为有些原生对象的内部属性,只有通过正确的 this 才能拿到,所以 Proxy 无法代理这些原生对象的属性。那么如何解决这个问题呢?要解决这个问题,我们可以为 getDate 方法绑定正确的 this:
- const target = new Date();
 - const handler = {
 - get(target, property, receiver) {
 - if (property === "getDate") {
 - return target.getDate.bind(target);
 - }
 - return Reflect.get(target, property, receiver);
 - },
 - };
 - const proxy = new Proxy(target, handler);
 - console.log(proxy.getDate());
 
5.4 创建可撤销的代理对象
通过 Proxy.revocable() 方法可以用来创建一个可撤销的代理对象,该方法的签名为:
- Proxy.revocable(target, handler);
 
相关的参数说明如下:
调用 Proxy.revocable 方法之后,其返回值是一个对象,其结构为:{"proxy": proxy, "revoke": revoke},其中:
了解完 revocable 方法之后,我们来举一个具体的示例:
- const target = {};
 - const handler = {};
 - const { proxy, revoke } = Proxy.revocable(target, handler);
 - proxy.name = "阿宝哥";
 - console.log(proxy.name); // 阿宝哥
 - revoke();
 - console.log(proxy.name); // TypeError: Revoked
 
当以上代码成功运行之后,控制台会输出以下内容:
- 阿宝哥
 - Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
 - at
 
通过观察以上的结果,我们可知当 proxy 对象被撤销之后,我们就没有办法对已撤销的proxy 对象执行任何操作。
六、Proxy 在开源项目中的应用
因为 Proxy 对象能够提供强大的拦截能力,所以它被应用在一些成熟的开源项目中,用于实现响应式的功能,比如 vue-next 和 observer-util 项目。对于 observer-util 这个项目,阿宝哥已经写了一篇 从观察者模式到响应式的设计原理 的文章来介绍该项目,感兴趣的小伙伴可以自行阅读。
而对于 vue-next 项目来说,响应式的功能被封装在 @vue/reactivity 模块中,该模块为我们提供了一个 reactive 函数来创建响应式对象。下面我们来简单了解一下 reactive函数的实现:
- // packages/reactivity/src/reactive.ts
 - export function reactive(target: object) {
 - if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
 - return target
 - }
 - return createReactiveObject(
 - target,
 - false,
 - mutableHandlers,
 - mutableCollectionHandlers
 - )
 - }
 
在 reactive 函数内部,会继续调用 createReactiveObject 函数来创建响应式对象,该函数也是被定义在 reactive.ts 文件中,该函数的的具体实现如下:
- // packages/reactivity/src/reactive.ts
 - function createReactiveObject(
 - target: Target,
 - isReadonly: boolean,
 - baseHandlers: ProxyHandler
 , - collectionHandlers: ProxyHandler
 - ) {
 - // 省略部分代码
 - const proxyMap = isReadonly ? readonlyMap : reactiveMap
 - const existingProxy = proxyMap.get(target)
 - if (existingProxy) {
 - return existingProxy
 - }
 - const proxy = new Proxy(
 - target,
 - targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
 - )
 - proxyMap.set(target, proxy)
 - return proxy
 - }
 
在 createReactiveObject 函数内部,我们终于见到了期待已久的 Proxy 对象。当 target 对象不是集合类型的对象,比如 Map、Set、WeakMap 和 WeakSet 时,在创建 Proxy 对象时,使用的是 baseHandlers,该 handler 对象定义了以下 5 种捕获器:
- export const mutableHandlers: ProxyHandler
 - get,
 - set,
 - deleteProperty,
 - has,
 - ownKeys
 - }
 
其中 get 和 set 捕获器是分别用于收集 effect 函数和触发 effect 函数的执行。好了,这里阿宝哥只是介绍一下 @vue/reactivity 中的 reactive 函数,关于该模块是如何实现响应式的细节,这里就不展开介绍了,阿宝哥后续会单独写一篇文章来详细分析该模块的功能。
七、参考资源MDN - Proxy
MDN - Reflect
exploringjs - proxies
stackoverflow - what-is-a-receiver-in-javascript
                标题名称:聊聊你不知道的Proxy
                
                URL网址:http://www.csdahua.cn/qtweb/news25/320625.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网