Proxy及Reflect
创建代理对象
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺 少其中任何一个参数都会抛出 TypeError。要创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象。
const target = { id: 'target' }; const handler = {}; const proxy = new Proxy(target, handler);复制代码
定义捕获器(概念来自操作系统)
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的 拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对 象之前先调用捕获器函数,从而拦截并修改相应的行为。类似之前的Object.defineProperty()
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 复制代码
捕获器参数和Reflect API
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数。有了这些参数,就可以重建被捕获方法的原始行为
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 方法。这些方法与捕获器拦截 的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。
const target = { foo: 'bar' }; const handler = { get() { return Reflect.get(...arguments); } }; // 可以更简洁 // 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 复制代码
反射 API 与对象 API
在使用反射 API 时,要记住: (1) 反射 API 并不限于捕获处理程序; (2) 大多数反射 API 方法在 Object 类型上有对应的方法。 通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。
以下反射方法都会提供状态标记:
Reflect.defineProperty()
Reflect.preventExtensions()
Reflect.setPrototypeOf()
Reflect.set()
Reflect.deleteProperty()
用一等函数替代操作符 以下反射方法提供只有通过操作符才能完成的操作。
Reflect.get():可以替代对象属性访问操作符。
Reflect.set():可以替代=赋值操作符。
Reflect.has():可以替代 in 操作符或 with()。
Reflect.deleteProperty():可以替代 delete 操作符。
Reflect.construct():可以替代 new 操作符。
代理模式
使用代理可以在代码中实现一些有用的编程模式
跟踪属性访问
通过捕获 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复制代码
隐藏属性
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 复制代码
属性验证
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 复制代码
函数与构造函数参数验证
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 复制代码
数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的 代码互操作。 比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
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
作者:用户4932111625461
链接:https://juejin.cn/post/7015883052349931533