• 【ECMAScript6】代理与反射


    一、代理与反射基础

    ECMAScript 6新增的代理和反射为开发者提供了拦截并向基本操作嵌入 额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对 象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的 各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

    1.1 空代理

    最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不 做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目 标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方 式来使用与之关联的代理对象。
    代理是使用Proxy构造函数创建的。这个构造函数接收两个参数:目标 对象和处理程序对象。缺少其中任何一个参数都会抛出TypeError。

            const target = { id: 'target' };
            const handler = {}; 
            const proxy = new Proxy(target, handler); // id属性会访问同一个值   
            console.log(target.id); // target 
            console.log(proxy.id); // target 
            // 给目标属性赋值会反映在两个对象上 
            // 因为两个对象访问的是同一个值 
            target.id = 'foo'; 
            console.log(target.id); // foo
            console.log(proxy.id); // foo
            // 给代理属性赋值会反映在两个对象上
            // 因为这个赋值会转移到目标对象
            proxy.id = 'bar'; 
            console.log(target.id); // bar 
            console.log(proxy.id); // bar
            // hasOwnProperty()方法在两个地方
            // 都会应用到目标对象
            console.log(target.hasOwnProperty('id')); // true
            console.log(proxy.hasOwnProperty('id')); // true
            // Proxy.prototype是undefined
            // 因此不能使用instanceof操作符
            console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check 
            console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype
            
            // 严格相等可以用来区分代理和目标
            console.log(target === proxy); // false
    
    • 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

    1.2 捕获器

    使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程 序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个 或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代 理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这 些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

            const target = { foo: 'bar' };
            const handler = { // 捕获器在处理程序对象中以方法名为键 
            get() { return 'handler override'; } };
            const proxy = new Proxy(target, handler);
    
    • 1
    • 2
    • 3
    • 4

    上面代码定义了一个get捕获器,当通过代理对象执行get()操作时,就会触发定义的get()捕获 器。proxy[property]、proxy.property或Object.create(proxy)[property]等操作都会触发基本的get()操作以获取属性。
    需要注意的是,只有在代理对象上执行这些操作才会出发捕获器,在目标对象上执行这些操作任然会产生正常的行为。

            const target = { foo: 'bar' };
            const handler = { 
            // 捕获器在处理程序对象中以方法名为键 
                get() { 
                    return 'handler override'; 
                } };
            const proxy = new Proxy(target, handler); 
            
            console.log(target.foo); // bar 
            console.log(proxy.foo); // handler override 
            
            console.log(target['foo']); // bar 
            console.log(proxy['foo']); // handler override
            
            console.log(Object.create(target)['foo']); // bar
            console.log(Object.create(proxy)['foo']); // handler override
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.3 捕获器参数与反射API

    所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法 的原始行为。比如,get()捕获器会接收到目标对象、要查询的属性和 代理对象三个参数。

            const target = { foo: 'bar' };
            
            const handler = { 
                get(trapTarget, property, receiver) { 
                    console.log(trapTarget === target); 
                    console.log(property); 
                    console.log(receiver === proxy); 
                } 
            };
            
            const proxy = new Proxy(target, handler);
            proxy.foo; 
            // true 
            // foo 
            // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有了这些参数,就可以重建被捕获方法的原始行为:

            const target = { foo: 'bar' };
            
            const handler = { 
                get(trapTarget, property, receiver) { 
                    return trapTarget[property]; 
                } 
            };
            const proxy = new Proxy(target, handler); 
            console.log(proxy.foo); // bar 
            console.log(target.foo); // bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像get()那么简单。因此,通过手动写码如法炮制的想法是不现实的。
    实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局Reflect对象上(封装了原始行为)的同名方法来轻松重建。

    处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方 法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也 具有与被拦截方法相同的行为。因此,使用反射API也可以像下面这样 定义出空代理对象:

            const target = { foo: 'bar' };
            const handler = { 
                get() { 
                    return Reflect.get(...arguments); 
                } 
            };
            const proxy = new Proxy(target, handler); 
            console.log(proxy.foo); // bar 
            console.log(target.foo); // bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    下面是更简单的写法:

            const target = { foo: 'bar' };
            const handler = { 
                get: Reflect.get
            };
            const proxy = new Proxy(target, handler); 
            console.log(proxy.foo); // bar 
            console.log(target.foo); // bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果真想创建一个可以捕获所有方法,然后将每个方法转发给 对应反射API的空代理,那么甚至不需要定义处理程序对象:

            const target = { foo: 'bar' };
            
            const proxy = new Proxy(target, Reflect); 
            
            console.log(proxy.foo); // bar 
            console.log(target.foo); // bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代理与反射的基本使用场景举例:

            const target = { foo: 'bar', baz: 'qux' };
            
            const handler = { 
                get(trapTarget, property, receiver) { 
                    let decoration = ''; 
                    if (property === 'foo') { 
                        decoration = '!!!'; 
                    }
                    return Reflect.get(...arguments) + decoration; 
                } 
            };
            
            const proxy = new Proxy(target, handler); 
            
            console.log(proxy.foo); // bar!!! 
            console.log(target.foo); // bar 
            console.log(proxy.baz); // qux 
            console.log(target.baz); // qux
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.4 捕获器不变式

    每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。

    比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError:

            const target = {}; 
            
            Object.defineProperty(target, 'foo', { 
                configurable: false, 
                writable: false, value: 'bar' 
            }); 
            
            const handler = { 
                get() { 
                    return 'qux'; 
                } 
            };
            const proxy = new Proxy(target, handler); 
            console.log(proxy.foo); // TypeError
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.5 可撤销代理

    有时候可能需要中断代理对象与目标对象之间的联系。对于使用new Proxy()创建的普通代理
    来说,这种联系会在代理对象的生命周期内一 直持续存在。
    Proxy也暴露了revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。撤销代理之后再调用代理会抛出 TypeError。

            const target = { foo: 'bar' };
            
            const handler = { 
                get() { 
                    return 'intercepted'; 
                } 
            };
            
            const { proxy, revoke } = Proxy.revocable(target, handler); 
            
            console.log(proxy.foo); // intercepted 
            console.log(target.foo); // bar revoke(); 
            console.log(proxy.foo); // TypeError
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.6 代理另一个代理

    代理可以拦截反射API的操作,而这意味着完全可以创建一个代理,通过它去代理另一个代理。这样就可以在一个目标对象之上构建多层拦截网:

            const target = { foo: 'bar' };
            
            const firstProxy = new Proxy(target, { 
                get() { 
                    console.log('first proxy'); 
                    return Reflect.get(...arguments); 
                } 
            }); 
            
            const secondProxy = new Proxy(firstProxy, { 
            get() { 
                console.log('second proxy'); 
                    return Reflect.get(...arguments); 
                } 
            }); 
            
            console.log(secondProxy.foo); 
            
            // second proxy 
            // first proxy 
            // bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    二、代理捕获器与反射方法

    • handler.get()
    • handler.set()
    • handler.has()
    • handler.defineProperty()
    • handler.getOwnPropertyDescriptor()
    • handler.deleteProperty()
    • handler.ownKeys()
    • handler.getPrototypeOf()
    • handler.setPrototypeOf()
    • handler.isExtensible()
    • handler.preventExtensions()
    • handler.apply()
    • handler.construct()

    API参考链接:
    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

    三、代理模式

    使用代理可以在代码中实现一些有用的编程模式。

    3.1 跟踪属性访问

    通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:

            const user = { name: 'Jake' };
            
            const proxy = new Proxy(user, { 
                get(target, property, receiver) { 
                    console.log(`Getting ${property}`); 
                    return Reflect.get(...arguments); 
                },
                set(target, property, value, receiver) { 
                    console.log(`Setting ${property}=${value}`); 
                    return Reflect.set(...arguments); 
                } 
            }); 
            
            proxy.name; 
            // Getting name 
            proxy.age = 27; 
            // Setting age=27
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.2 隐藏属性

    代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。

            const hiddenProperties = ['foo', 'bar']; 
            
            const targetObject = { foo: 1, bar: 2, baz: 3 };
            
            const proxy = new Proxy(targetObject, { 
                get(target, property) { 
                    if (hiddenProperties.includes(property)) { 
                        return undefined; 
                    } else { 
                        return Reflect.get(...arguments); 
                    }
                },has(target, property) { 
                    if (hiddenProperties.includes(property)) { 
                        return false; 
                    } else { 
                        return Reflect.has(...arguments); 
                    } 
                } 
            }); 
            
            
            // get() 
            console.log(proxy.foo); // undefined 
            console.log(proxy.bar); // undefined 
            console.log(proxy.baz); // 3 
            
            // has() 
            console.log('foo' in proxy); // false 
            console.log('bar' in proxy); // false 
            console.log('baz' in proxy); // true
    
    • 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

    3.3 属性验证

    因为所有赋值操作都会触发set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:

            const target = { onlyNumbersGoHere: 0 };
            
            const proxy = new Proxy(target, { 
                set(target, property, value) { 
                if (typeof value !== 'number') { 
                    return false; 
                } else { 
                    return Reflect.set(...arguments); 
                } 
                } 
            }); 
            
            proxy.onlyNumbersGoHere = 1; 
            console.log(proxy.onlyNumbersGoHere); // 1 
            proxy.onlyNumbersGoHere = '2'; 
            console.log(proxy.onlyNumbersGoHere); // 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.4 函数与构造函数参数验证

    跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值:

            function median(...nums) { 
                return nums.sort()[Math.floor(nums.length / 2)]; 
            }
            const proxy = new Proxy(median, { 
                apply(target, thisArg, argumentsList) { 
                    for (const arg of argumentsList) { 
                        if (typeof arg !== 'number') { 
                            throw 'Non-number argument provided'; 
                        } 
                    }
                    return Reflect.apply(...arguments); 
                }
            }); 
            
            console.log(proxy(4, 7, 1)); // 4 
            console.log(proxy(4, '7', 1)); // Error: Non-number argument provided
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    类似地,可以要求实例化时必须给构造函数传参:

            class User { 
                constructor(id) { 
                    this.id_ = id; 
                } 
            }
            
            const proxy = new Proxy(User, { 
                construct(target, argumentsList, newTarget) { 
                    if (argumentsList[0] === undefined) { 
                        throw 'User cannot be instantiated without id'; 
                    } else { 
                        return Reflect.construct(...arguments); 
                    } 
                } 
            }); 
            
            new proxy(1); 
            new proxy(); // Error: User cannot be instantiated without id
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.5 数据绑定与可观察对象

    通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。
    比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:

            const userList = []; 
            
            class User { 
                constructor(name) { 
                    this.name_ = name; 
                } 
            }
            
            const proxy = new Proxy(User, { 
                construct() { 
                    const newUser = Reflect.construct(...arguments); 
                    userList.push(newUser); return newUser; 
                } 
            }); 
            
            new proxy('John'); 
            new proxy('Jacob'); 
            new proxy('Jingleheimerschmidt'); 
            
            console.log(userList); // [User {}, User {}, User{}]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会 发送消息:

    const userList = []; 
    
    function emit(newValue) { 
        console.log(newValue); 
    }
    
    const proxy = new Proxy(userList, { 
        set(target, property, value, receiver) { 
            const result = Reflect.set(...arguments); 
            if (result) { 
                emit(Reflect.get(target, property, receiver)); 
            }
            return result; 
        } 
    }); 
    
    proxy.push('John'); 
    // John 
    proxy.push('Jacob'); 
    // Jacob
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    四、总结

    从宏观上看,代理是真实JavaScript对象的透明抽象层。代理可以定义包 含捕获器的处理程序对象,而这些捕获器可以拦截绝大部分JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任何基本操作 的行为,当然前提是遵从捕获器不变式。

    与代理如影随形的反射API,则封装了一整套与捕获器拦截的操作相对 应的方法。可以把反射API看作一套基本操作,这些操作是绝大部分 JavaScript对象API的基础。

    代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模 式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除 属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。

  • 相关阅读:
    【linux】初识进程
    elasticsearch 映射
    ESP8266三种工作模式以及TCP/UDP服务
    TP6首页加载报错 Call to a member function run() on null
    测试为什么分白盒、黑盒、单元、集成测试?
    神经系统类型图片高清,神经系统类型图片解析
    第 113 场 LeetCode 双周赛题解
    申诉解决TeamViewer免费个人版被误判为商业使用
    vr模拟电力场景安全应急培训,电力安全教育培训新方法
    思科拟推出PuzzleFS驱动,采用Rust语言开发
  • 原文地址:https://blog.csdn.net/qq_41481731/article/details/125538547