代理是目标对象的抽象。 目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理。
使用 Proxy 构造函数创建代理,该函数接收目标对象 和处理程序对象 作为参数。
创建空代理 空代理除了作为一个抽象的目标对象,什么也不做。在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。
可以向 Proxy 构造函数传递一个简单的对象字面量作为处理程序对象,来创建空代理。
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 31 32 33 34 const target = { id : 'target' };const handler = {};const proxy = new Proxy (target, handler);console .log (target.id ); console .log (proxy.id ); target.id = 'foo' ;console .log (target.id ); console .log (proxy.id ); proxy.id = 'bar' ;console .log (target.id ); console .log (proxy.id ); console .log (target.hasOwnProperty ('id' )); console .log (proxy.hasOwnProperty ('id' )); console .log (target instanceof Proxy );console .log (proxy instanceof Proxy );console .log (target === proxy);
定义捕获器
使用代理的主要目的是定义捕获器(trap)
捕获器是在处理程序对象中定义的“基本操作的拦截器”
每个处理程序对象可以包含零个或多个捕获器 ,每个捕获器都对应一种基本操作,代理对象可以直接或间接调用
代理可以在基本操作传播到目标对象之前先调用捕获器函数,拦截并修改相应的行为
捕获器在操作系统中是程序流的一个同步中断,可以暂停程序流,转而执行一段子例程,再返回原始程序流
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 31 const target = { foo : 'bar' };const handler = { get ( ) { return 'handler override' ; } };const proxy = new Proxy (target, handler);console .log (target.foo ); console .log (proxy.foo ); console .log (target['foo' ]); console .log (proxy['foo' ]); console .log (Object .create (target)['foo' ]); console .log (Object .create (proxy)['foo' ]);
捕获器参数和反射 API 所有捕获器都可以访问相应的参数,get() 捕获器能接收到目标对象、要查询的属性和代理对象。
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 31 const target = { foo : 'bar' };const handler = { get (trapTarget, property, receiver ) { return trapTarget[property]; } };const proxy = new Proxy (target, handler);console .log (proxy.foo ); console .log (target.foo );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const target = { foo : 'bar' };const handler = { get : Reflect .get };const proxy = new Proxy (target, handler);console .log (proxy.foo ); console .log (target.foo );
不需要定义处理程序对象就可以创建一个可以捕获所有方法,并将每个方法转发给对应反射 API 的空代理
1 2 3 4 5 6 7 8 const target = { foo : 'bar' };const proxy = new Proxy (target, Reflect );console .log (proxy.foo ); console .log (target.foo );
可以在反射 API 样板代码的基础上用最少的代码修改捕获的方法
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 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, Reflect );console .log (proxy.foo ); console .log (target.foo ); console .log (proxy.baz ); console .log (target.baz );
捕获器的限制 每个捕获的方法都知道目标对象上下文、捕获函数签名 ,捕获处理程序的行为必须遵循 “捕获器不变式 ”(trap invariant)。
如果目标对象有一个不可配置且不可写的数据属性,捕获器返回一个与该属性不同的值,会抛出 TypeError。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 );
撤销代理 Proxy 的 revocable() 方法支持撤销代理对象与目标对象的关联,且操作不可逆。撤销代理后再调用代理会抛出 TypeError。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const target = { foo : 'bar' };const handler = { get ( ) { return 'intercepted' ; } };const { proxy, revoke } = Proxy .revocable (target, handler);console .log (proxy.foo ); console .log (target.foo ); revoke ();console .log (proxy.foo );
反射 API 以下是优先使用反射 API 的情况
反射 API 与对象 API Object 的方法适用于通用程序,反射方法适用于细粒度的对象控制与操作
状态标记 反射方法返回称作“状态标记 ”的布尔值,表示执行的操作是否成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const o = {};try { Object .defineProperty (o, 'foo' , 'bar' ); console .log ('success' ); } catch (e) { console .log ('failure' ); }const o = {};if (Reflect .defineProperty (o, 'foo' , {value : 'bar' })) { console .log ('success' ); } else { console .log ('failure' ); }
Reflect.defineProperty()、Reflect.preventExtensions()、Reflect.setPrototypeOf()、Reflect.set() 和 Reflect.deleteProperty() 都会提供状态标记
替代操作符 Reflect.get()(可以替代对象属性访问操作符)、Reflect.set()(可以替代 = 赋值操作符)、Reflect.has()(可以替代 in 操作符或 with())、Reflect.deleteProperty()(可以替代 delete 操作符)和 Reflect.construct()(可以替代 new 操作符)等反射方法提供只有通过操作符才能完成的操作
安全地使用函数 为了绕过使用 apply 方法调用函数时,被调函数也定义了 apply 属性的情况(直接使用 对象.apply(...) 调用函数时,会优先访问函数自身的 apply 属性),可使用定义在 Function 原型上的 apply 方法(强制使用 JavaScript 引擎内置的函数调用逻辑,无论被调函数是否有 apply 属性,都能保证正确执行目标函数)
1 2 3 4 Function .prototype .apply .call (myFunc, thisVal, argumentList);Reflect .apply (myFunc, thisVal, argumentList);
代理另一个代理 可以通过一个代理去代理另一个代理,在一个目标对象上构建多层拦截网
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 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 );
代理的问题 代理作为对象的虚拟层可以正常使用,但某些情况下不能与现有的机制协同
潜在问题来源 —— this 值 如果代理的目标对象依赖于对象标识 (判断两个对象是否是同一个实例的机制,两个变量是否指向内存中的同一个对象),可能会碰到问题。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const target = { thisValEqualsProxy ( ) { return this === proxy; } };const proxy = new Proxy (target, {});console .log (target.thisValEqualsProxy ()); console .log (proxy.thisValEqualsProxy ()); const wm = new WeakMap ();class User { constructor (userId ) { wm.set (this , userId); } set id (userId ) { wm.set (this , userId); } get id () { return wm.get (this ); } }const user = new User (123 );console .log (user.id ); const UserClassProxy = new Proxy (User , {});const proxyUser = new UserClassProxy (456 );console .log (proxyUser.id );
代理与内部槽位 有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,导致在代理上调用某些方法出错。
比如,Date 类型方法的执行依赖 this 值的内部槽位 [[NumberDate]],但代理对象不存在该内部槽位,代理拦截后会抛出 TypeError。
1 2 3 4 5 6 const target = new Date ();const proxy = new Proxy (target, {});console .log (proxy instanceof Date ); proxy.getDate ();
内部槽位是 ECMA 规范定义的、对象内部用于存储状态或特性的特殊“容器”,不是对象的属性,无法直接访问或修改。内部槽位仅在引擎层面存在,用于实现语言的核心功能