代理与反射

代理的目标是使得代理对象上执行的任何操作都应用到目标对象上,对于开发者来说唯一可感知的就是代码中操作的对象是代理对象。

代理基础

空代理

const target = {
  id: 1
};

const handler = {};

const proxy = new Proxy(target, handler);

// 代理与目标不完全相等
console.log(target === proxy); // false

// 目标和代理都会有 id
console.log(proxy.hasOwnProperty('id')); // true
console.log(target.hasOwnProperty('id')); // true

console.log(target.id); // 1
console.log(proxy.id); // 1

// 目标对象属性赋值会影响代理对象
target.id = 2;
console.log(target.id); // 2
console.log(proxy.id); // 2

// 代理对象属性赋值会颖目标对象
proxy.id = 3;
console.log(target.id); // 3
console.log(proxy.id); // 3

// 注意不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError
console.log(proxy instanceof Proxy); // TypeError

定义捕获器

const target = {
  id: 1
};

const handler = {
  get() {
    return 'handler override';
  }
};

const proxy = new Proxy(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // handle override

捕获器参数与反射 API

const target = {
  id: 1
};

const handler = {
  get(trapTarget, property, receiver) {
    console.log(trapTarget === target);
    console.log(property);
    console.log(receiver === proxy);
  }
};

const proxy = new Proxy(target, handler);

proxy.id;
// true
// id
// true

通过参数即可执行操作

const target = {
  id: 1
};

const handler = {
  get(trapTarget, property) {
    return trapTarget[property];
  }
};

const proxy = new Proxy(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // 1

所有捕获器都可以基于自己的参数重写原来的操作,但是并非所有的捕获器操作都像 get() 那么简单,所以通过手动实现所有代理的想法是不实际的。实际上,开发者不需要自己手动重写原来的操作,而是可以通过调用全局对象 Reflect 上的同名对象实现相同的效果。

const target = {
  id: 1
};

const handler = {
  get() {
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // 1

Reflect API 实际上为用户准备好了一套模板代码,使得开发者可以在此基础上使用最少的代码实现对捕获器的重写操作。

const target = {
  id: 1,
  name: 'xiaoming'
};

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

const proxy = new Proxy(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // 1

console.log(target.name); // xiaoming
console.log(proxy.name); // xiaoming!

捕获器不变式

如果对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性值不同的值时,会抛出一个 TypeError。

const target = {};

Object.defineProperty(target, 'id', {
  writable: false,
  configurable: false,
  value: 1
});

const handler = {
  get() {
    return 2;
  }
};

const proxy = new Proxy(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // TypeError

可撤销代理

撤销之后再调用代理会报错。

const target = {
  id: 1
};

const handler = {
  get() {
    return 2;
  }
};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(target.id); // 1
console.log(proxy.id); // 2

revoke();

console.log(target.id); // 1
console.log(proxy.id); // TypeError

代理另一个代理

代理也可以代理另一个代理,从而实现多重代理的效果。

代理的问题与不足

在某些情况下,代理无法与现在的 ECMAScript 很好地协同

常见的有因为 this 指向导致 WeakMap 存在的问题,以及关于内置数据类型 Date 导致的 bug。

const target = new Date();

const handler = {};

const proxy = new Proxy(target, handler);

console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: this is not a Date object

代理捕获器与反射方法

get()

const target = {
  id: 1
};

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

proxy.id;

// get()

set()

const target = {
  id: 1
};

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

proxy.id = 2;

// set()

has()

const target = {
  id: 1
};

const proxy = new Proxy(target, {
  has() {
    console.log('has()');
    return Reflect.has(...arguments);
  }
});

'id' in proxy;

// has()

defineProperty()

const target = {};

const proxy = new Proxy(target, {
  defineProperty() {
    console.log('defineProperty()');
    return Reflect.defineProperty(...arguments);
  }
});

Object.defineProperty(proxy, 'id', {
  value: 1
});

// defineProperty()

类似的还有 getOwnPropertyDescriptor() deleteProperty() ownKeys() getPrototypeOf() setPrototypeOf() isExtensible() preventExtensions() apply() construct()

代理模式

跟踪属性访问

通过捕获 get() set() 等操作,可以知道对象属性什么时候被访问、被修改。

const user = {
  id: 1
};

const proxy = new Proxy(user, {
  get(_target, property) {
    console.log(`Get ${property}`);
    return Reflect.get(...arguments);
  },
  set(_target, property, value) {
    console.log(`Set ${property} = ${value}`);
    return Reflect.set(...arguments);
  }
});

proxy.id; // Get id
proxy.id = 2; // Set id = 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

属性验证与参数验证

因为所有的赋值操作都会触发 set() 捕获器,所以可以根据所赋的值决定是否允许赋值。

const target = {
  onlyNumbersHoHere: 0
};

const proxy = new Proxy(target, {
  set(_target, _property, value) {
    if (typeof value !== 'number') {
      console.log('fail to set the value');
      return false;
    } else {
      return Reflect.set(...arguments);
    }
  }
});

proxy.onlyNumbersHoHere = 1;
console.log(proxy.onlyNumbersHoHere); // 1

proxy.onlyNumbersHoHere = '2';
console.log(proxy.onlyNumbersHoHere); // 1

类似地,也可以对函数的参数进行验证。

数据绑定与可观察对象

通过代理可以把运行时中原本不相关的部分联系到一起,这样就可以实现各种模式,从而让不同的代码互操作。 比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中。

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('Jack');
new proxy('Mary');

console.log(userList);

// [
//   User { _name: 'John' },
//   User { _name: 'Jack' },
//   User { _name: 'Mary' }
// ]