Skip to content

Proxy

Proxy对象提供了JS可以在更高层面操控对象的可能,它没有自己的状态和行为,只负责将外界的操作传递给原始对象,本质是代理模式。

对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。

拦截外界的操作遵循以下步骤:

  1. 首先查询handlers中是否存在相应的handler,如果有,则调用它操作原始对象
  2. 反之,则将操作传递给原始对象。

Proxy 的局限性

许多内建对象,例如 MapSetDatePromise 等,都使用了所谓的“内部插槽”

它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们。

例如:

js
let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('test', 1); // TypeError: Method Map.prototype.set called on incompatible receiver #<Map>

可以如此解决:

js
let map = new Map();

let proxy = new Proxy(map, {
  get(target, p, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value === 'function' ? value.bind(target) : value;
  },
});

proxy.set('test', 1);

WARNING

注意:handlers中劫持的是get方法,调用时是set方法

理论上说,Map对象和一般对象并没有区别。从外部看,map.get('test') 是两个基本语义的组合

  • map_get = Get(map, 'get')
  • Apply(map_get, map, ['test'])

落实到具体的Map对象上,第一步默认情况下是获得Map.prototype.get方法, 第二步默认相当于执行Map.prototype.get.apply(map, ['test'])

真正调用内部槽的是Map.prototype.get方法的内部实现,它会存取发送给它的this参数(map对象)上的[[MapData]]内部槽(因此,假若你传入的this参数不是Map的实例对象从而没有该内部槽,就会扔TypeError)。

在该例中,由于proxy对象上没有对应的内部插槽,导致在执行Map.prototype.get时无法访问到而抛出TypeError

存取内部槽总是一个内部实现细节,从对象的使用者角度说,是无从知晓的。

当然,虽说理论上内部槽是不可观测的,但实践上我们可以近似判断:一个存取了内部槽的方法是不能简单地代理调用的。

假设有对象o,let p = new Proxy(o, {})

  • 对于普通对象,p.method()和o.method()结果通常是一致的,此即所谓代理透明性。
  • 但如果method访问了o的内部槽,由于p(代理对象)并没有o的那些内部槽 ,故而会抛TypeError。

注意,我们只能猜测o有内部槽而不可能严格断言,因为方法可能由于任何原因抛TypeError,或者访问内部槽的代码可以通过测试是否具有内部槽或catch掉error来不抛TypeError。

WARNING

使用Proxy代理有内部插槽(internal slot)的对象时,会出现古怪的现象,因为内部插槽对Proxy来说是不透明的。

可撤销的Proxy

Proxy提供了一个可撤销代理的方法:Proxy.revocable()。它返回一个对象,包含两个属性,分别是:proxy、revoke

  • proxy:代理原始对象的proxy对象
  • revoke:撤销按钮
js
function sayHello() {
  console.log(123);
}
const { proxy, revoke } = Proxy.revocable(sayHello, {});

proxy(); // 123
revoke();
proxy(); // TypeError: Cannot perform 'apply' on a proxy that has been revoked

Proxy.revocable()的使用场景多为:当你要使用不信任的第三方依赖时需要将向它传递一个函数,那么可以执行proxy之后再执行revoke撤销代理。多用于防止第三方劫持你函数的引用,是防御编程的一种。

proxy invariants

虽然通过Proxy对象可以精细化管理引用数据类型的操作,但是不能定义明显错误的行为。Proxy类会对结果进行检查,防止出现违背JS invariant的行为

js
let target = Object.freeze({ x: 1 });
let proxy = new Proxy(target, {
  get(target, p, receiver) {
    return 99;
  },
});

console.log(proxy.x);
// TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '1' but got '99')

参考:

【1】Proxy 和 Reflect

【2】Proxy