【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 捕获器

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

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

上面代码定义了一个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.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

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

        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

所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像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

下面是更简单的写法:

        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

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

        const target = { foo: 'bar' };
        
        const proxy = new Proxy(target, Reflect); 
        
        console.log(proxy.foo); // bar 
        console.log(target.foo); // bar

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

        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.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.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.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

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

  • 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

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

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

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

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

        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

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{}]

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

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

四、总结

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值