第9章 代理与反射

ECMAScript 6新增代理和反射,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对
目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的 ECMAScript 代码,只在百分之百支持它们的平台上有用。

1 代理基础

代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。
注意 ECMAScript 代理与 C++指针有重大区别。指针在概念上易于理解。

1.1 创建空代理

代理是使用 Proxy 构造函数创建的。这个构造函数接收两个必要参数:目标对象和处理程序对象。如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象:

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 
'undefined' in instanceof check 
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

1.2 定义捕获器

使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。例如定义一个 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

proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。

1.3 捕获器参数和反射 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

开发者并不需要手动重建原始行为,可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来重建。处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。

const target = { 
 foo: 'bar' 
};
const handler = { 
 get: Reflect.get 
 // 可以这样写
 //  get() { 
 // return Reflect.get(...arguments); 
//  }
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

在此基础上开发者可以用最少的代码修改捕获的方法:

const handler = { 
 get(trapTarget, property, receiver) { 
 let decoration = ''; 
 if (property === 'foo') { 
 decoration = '!!!'; 
 } 
 return Reflect.get(...arguments) + decoration; 
 } 
};

1.4 捕获器不变式

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 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 可撤销代理

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

1. 反射 API 与对象 API

(1) 反射 API 并不限于捕获处理程序;
(2) 大多数反射 API 方法在 Object 类型上有对应的方法。
通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。

2.状态标记

很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用。例如,可以使用反射API 对下面的代码进行重构:

// 在定义新属性时如果发生问题,Reflect.defineProperty()会返回 false,而不是抛出错误。
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()

3. 用一等函数替代操作符

以下反射方法提供只有通过操作符才能完成的操作。
 Reflect.get():可以替代对象属性访问操作符。
 Reflect.set():可以替代=赋值操作符。
 Reflect.has():可以替代 in 操作符或 with()。
 Reflect.deleteProperty():可以替代 delete 操作符。
 Reflect.construct():可以替代 new 操作符。

4.安全地应用函数

在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性(虽然可能性极小)。为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:
Function.prototype.apply.call(myFunc, thisVal, argumentList);

这种可怕的代码完全可以使用 Reflect.apply 来避免:
Reflect.apply(myFunc, thisVal, argumentsList);

1.7 代理另一个代理

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.8 代理的问题与不足

1.代理中的 this

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); 
 } 
}

由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况下就会出问题:

const user = new User(123); 
console.log(user.id); // 123 
const userInstanceProxy = new Proxy(user, {}); 
console.log(userInstanceProxy.id); // undefined
// 这是因为 User 实例一开始使用目标对象作为 WeakMap 的键,代理对象却尝试从自身取得这个实
//例。

解决方法,把代理 User 实例改为代理 User 类本身。之后再创建代
理的实例就会以代理实例作为 WeakMap 的键了:

const UserClassProxy = new Proxy(User, {}); 
const proxyUser = new UserClassProxy(456); 
console.log(proxyUser.id);

2.代理与内部槽位

Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const target = new Date(); 
const proxy = new Proxy(target, {}); 
console.log(proxy instanceof Date); // true 
proxy.getDate(); // TypeError: 'this' is not a Date object

2 代理捕获器与反射方法

2.1 get()

get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 get(target, property, receiver) { 
 console.log('get()'); 
 return Reflect.get(...arguments) 
 } 
}); 
proxy.foo; 
// get()

1. 返回值

返回值无限制。

2. 拦截的操作

 proxy.property
 proxy[property]
 Object.create(proxy)[property]
 Reflect.get(proxy, property, receiver)

3. 捕获器处理程序参数

 target:目标对象。
 property:引用的目标对象上的字符串键属性。
 receiver:代理对象或继承代理对象的对象。

4. 捕获器不变式

如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配。如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined。

2.2 set()

对应的反射 API 方法为 Reflect.set()。

const proxy = new Proxy(myTarget, { 
 set(target, property, value, receiver) { 
 console.log('set()'); 
 return Reflect.set(...arguments) 
 } 
});

1. 返回值

返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError

2. 拦截的操作

 proxy.property = value
 proxy[property] = value
 Object.create(proxy)[property] = value
 Reflect.set(proxy, property, value, receiver)

3. 捕获器处理程序参数

 target:目标对象。
 property:引用的目标对象上的字符串键属性。
 value:要赋给属性的值。
 receiver:接收最初赋值的对象。

4. 捕获器不变式

如果 target.property 不可写且不可配置,则不能修改目标属性的值。如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。在严格模式下,处理程序中返回 false 会抛出 TypeError。

2.3 has()

has()捕获器会在 in 操作符中被调用。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 has(target, property) { 
 console.log('has()'); 
 return Reflect.has(...arguments)  // 必须返回布尔值
 } 
}); 
'foo' in proxy;  // 拦截的操作: in、with(proxy) {(property);}
// has()

2.4 defineProperty()

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 defineProperty(target, property, descriptor) { 
 console.log('defineProperty()'); 
 return Reflect.defineProperty(...arguments)  // 返回布尔值,表示属性是否成功定义。会被转换为布尔值
 } 
}); 
// 拦截Object.defineProperty、Reflect.defineProperty
Object.defineProperty(proxy, 'foo', { value: 'bar' }); 
// defineProperty()

2.5 apply()

apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。

const myTarget = () => {}; 
const proxy = new Proxy(myTarget, { 
 apply(target, thisArg, ...argumentsList) { 
 console.log('apply()'); 
 return Reflect.apply(...arguments) 
 } 
}); 
proxy(); 
// apply()

返回值无限制,拦截的操作:
 proxy(…argumentsList)
 Function.prototype.apply(thisArg, argumentsList)
 Function.prototype.call(thisArg, …argumentsList)
 Reflect.apply(target, thisArgument, argumentsList)

参数:
 target:目标对象
 thisArg:调用函数时的 this 参数。
 argumentsList:调用函数时的参数列表

2.6 其他的

  • getOwnPropertyDescriptor()
  • deleteProperty()
  • ownKeys()
  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • construct()

3 代理模式

3.1 跟踪属性访问

通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。

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

4 小结

代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地。

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

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

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

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值