JavaScript高级程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

9.3 代理模式

使用代理可以在代码中实现一些有用的编程模式。

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

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

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

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

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