概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

  1. var obj = new Proxy({}, {
  2. get: function (target, key, receiver) {
  3. console.log(`getting ${key}!`);
  4. return Reflect.get(target, key, receiver);
  5. },
  6. set: function (target, key, value, receiver) {
  7. console.log(`setting ${key}!`);
  8. return Reflect.set(target, key, value, receiver);
  9. }
  10. });
  11. obj.count = 1
  12. // setting count!
  13. ++obj.count
  14. // getting count!
  15. // setting count!
  16. // 2

proxy对象的使用

  1. let p = new Proxy(target, handler);
  2. target
  3. Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  4. handler proxy的处理程序对象
  5. 一个对象,其属性是当执行一个操作时定义代理的行为的函数。

handler的方法

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/apply
参考 MDN

get(target, propKey, receiver)

target 目标对象,propKey 访问的key,receiver Proxy或者继承Proxy的对象
return any; 返回一个任意值
拦截对象属性的读取

set(target, propKey, value, receiver)

target 目标对象,propKey 访问的key,value 设置的值,receiver Proxy或者继承Proxy的对象
return any; 返回一个任意值
拦截对象属性的设置

has(target, propKey)

target 目标对象,propKey 可以是字符串或symbol 类型
return boolean;
方法可以看作是针对 in 操作的钩子.

  1. var obj = { a: 1 };
  2. a in obj // true

deleteProperty(target, propKey);

target 目标对象,propKey 访问的key
返回一个boolean 表示是否删除成功
拦截对对象属性的 delete 操作,返回false属性不可删除

ownKeys(target)

target 目标对象
返回一个可枚举对象
该拦截器可以拦截以下操作::

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()

    getOwnPropertyDescriptor(target, propKey)

    target 目标对象,propKey 访问的key
    返回属性名称的描述
    方法是 Object.getOwnPropertyDescriptor() 的拦截。

    defineProperty(target, propKey, descriptor)

    target 目标对象,propKey key的名称,属性描述符
    方法必须以一个 Boolean 返回,表示定义该属性的操作成功与否
    该方法会拦截目标对象的以下操作 :

  • Object.defineProperty()

  • Reflect.defineProperty()
  • Object.defineProperties()

    apply(target, thisArg, argumentsList)

    target 目标对象 thisArg 被调用时的上下文对象 argumentsList 参数列表
    方法可以返回任何值
    拦截函数调用

    construct(target, argumentsList, newTarget)

    target 目标对象,argumentsList 参数列表, 最初被调用的构造函数
    方法必须返回一个对象
    方法用于拦截new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。

    getPrototypeOf(target)

    target 目标对象
    方法必须返回一个对象或null,不能返回其他值,会抛出异常。
    方法拦截Object.getPrototypeOf方法。

    isExtensible(target)

    target 目标对象
    方法必须返回一个Boolean或一个可以转换为Boolean的值
    该方法会拦截目标对象的以下操作:

  • Object.isExtensible()检查方法是否是可扩展的,可扩展的返回true,密封,冻结,不可扩展返回false

  • Reflect.isExtensible()

    preventExtensions(target)

    target目标对象
    返回一个boolean
    拦截 Object.preventExtensions()返回一个布尔值

    setPrototypeOf(target, newPrototype)

    target 目标对象, newPrototype 新的原型
    如果成功修改了[[Prototype]], setPrototypeOf 方法返回 true,否则返回 false.
    方法主要用来拦截 Object.setPrototypeOf().

Proxy.revocable()

Proxy.revocable方法返回一个可取消的 Proxy 实例。

  1. let target = {};
  2. let handler = {};
  3. let {proxy, revoke} = Proxy.revocable(target, handler);
  4. proxy.foo = 123;
  5. proxy.foo // 123
  6. revoke();
  7. proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。
Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

  1. const target = {
  2. m: function () {
  3. console.log(this === proxy);
  4. }
  5. };
  6. const handler = {};
  7. const proxy = new Proxy(target, handler);
  8. target.m() // false
  9. proxy.m() // true

上面代码中,一旦proxy代理target.m,后者内部的this就是指向proxy,而不是target
下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。

  1. const _name = new WeakMap();
  2. class Person {
  3. constructor(name) {
  4. _name.set(this, name);
  5. }
  6. get name() {
  7. return _name.get(this);
  8. }
  9. }
  10. const jane = new Person('Jane');
  11. jane.name // 'Jane'
  12. const proxy = new Proxy(jane, {});
  13. proxy.name // undefined

上面代码中,目标对象janename属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name访问时,this指向proxy,导致无法取到值,所以返回undefined
此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

  1. const target = new Date();
  2. const handler = {};
  3. const proxy = new Proxy(target, handler);
  4. proxy.getDate();
  5. // TypeError: this is not a Date object.

上面代码中,getDate方法只能在Date对象实例上面拿到,如果this不是Date对象实例就会报错。这时,this绑定原始对象,就可以解决这个问题。

  1. const target = new Date('2015-01-01');
  2. const handler = {
  3. get(target, prop) {
  4. if (prop === 'getDate') {
  5. return target.getDate.bind(target);
  6. }
  7. return Reflect.get(target, prop);
  8. }
  9. };
  10. const proxy = new Proxy(target, handler);
  11. proxy.getDate() // 1

实例:Web 服务的客户端

Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

  1. const service = createWebService('http://example.com/data');
  2. service.employees().then(json => {
  3. const employees = JSON.parse(json);
  4. // ···
  5. });

上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

  1. function createWebService(baseUrl) {
  2. return new Proxy({}, {
  3. get(target, propKey, receiver) {
  4. return () => httpGet(baseUrl+'/' + propKey);
  5. }
  6. });
  7. }

同理,Proxy 也可以用来实现数据库的 ORM 层。