为什么 Proxy 会破坏 this-binding?

Why does Proxy break this-binding?

我正在尝试代理一组对象,以便我可以将它们传递给第三方代码并暂时使突变方法和设置器无效,然后撤销代理处理程序陷阱以恢复正常行为。我发现代理本质上对依赖 this 的代码怀有敌意。

我很好奇 Javascript 代理如何以及为什么会破坏 this 对其代理目标的绑定。在下面的示例中,我有一个简单的 class,它在构造时获取一个值,将其存储为私有字段,并在 属性 访问时 returns 它。观察到:

  1. 在没有处理程序的情况下尝试通过代理访问 属性 会引发错误
  2. 通过 Reflect.get 显式转发处理程序 get 陷阱恢复正常行为

class Thing {
  #value

  constructor(value){
    this.#value = value
  }
  
  get value(){
    return this.#value
  }
}

// Default behaviour
const thing1 = new Thing('foo')

attempt(() => thing1.value)

// No-op proxy breaks contextual access behaviour
const proxy1 = new Proxy(thing1, {})

attempt(() => proxy1.value)

// Reinstated by explicitly forwarding handler get call to Reflect
const proxy2 = new Proxy(thing1, {get: (target, key) =>
  Reflect.get(target, key)
})

attempt(() => proxy2.value)

function attempt(fn){
  try {
    console.log(fn())
  }
  catch(e){
    console.error(e)
  }
}

这为 getter 访问提供了一种解决方法,但我不明白为什么会出现问题或为什么额外的代码可以解决问题。当涉及到方法时,未处理的代理查询中上下文冲突的相同问题更加令人烦恼。在下面的代码中,value 属性 成为方法而不是 getter。在这种情况下:

  1. 默认行为仍然被破坏
  2. Reflect.get 不再有效
  3. 可以get陷阱
  4. 中显式绑定this
  5. 但这并不是预期行为的恢复

class Thing {
  #value

  constructor(value){
    this.#value = value
  }
  
  value(){
    return this.#value
  }
}

// Default behaviour
const thing1 = new Thing('foo')

attempt(() => thing1.value())

// No-op proxy breaks contextual access behaviour
const proxy1 = new Proxy(thing1, {})

attempt(() => proxy1.value())

// Forwarding handler get trap to Reflect doesn't work
const proxy2 = new Proxy(thing1, {get: (target, key) =>
  Reflect.get(target, key)
})

attempt(() => proxy2.value())

// Explicitly binding the returned method *does* work
const proxy3 = new Proxy(thing1, {get: (target, key) =>
  target[key].bind(target)
})

attempt(() => proxy3.value())

// But this goes beyond reinstating normal behaviour
var {value} = thing1

attempt(() => value())

var {value} = proxy3

attempt(() => value())

function attempt(fn){
  try {
    console.log(fn())
  }
  catch(e){
    console.error(e)
  }
}

TDLR;

  1. 私有访问需要将操作上下文设置为创建私有成员的对象(提供代理解决方案)
  2. 对于您提供的用例和人为设计的代码,不需要代理,因为可以使用简单的继承来实现目标(最底部的解决方案)

第一个例子

第一个示例中的 no-op Proxy 没有损坏。 get() 方法仍然通过 Proxy 对象调用(甚至是无操作),而不是 thing 对象。因此私有成员无法通过 proxy1.value 的代理访问。您在第一个示例中的修复是使用反射 the common way 以几乎所有具有访问限制的语言访问这些成员(有些需要屈折变化)。从历史上看,在 Reflect.get() 可用之前,这是使用函数对象的 .apply() 方法完成的。因此,出于同样的原因,使用 Reflect.get() 是有意义的。

底线:

因此您必须采取一些措施将上下文设置为创建私有成员的对象,否则您将无法访问它。

第二个例子

Reflect.get() 的调用在第二个示例中不起作用,因为将 getter 语法从 get value() 转移到 value()。现在调用一个函数来检索 value 它必须绑定到正确的对象。简单的反思是不够的。要使 Reflect.get() 在这里工作,您必须将 getter 函数绑定到目标。

使用函数的 .bind() 方法是另一种控制操作上下文的传统方法。来自 docs:

The bind() method creates a new function that, when called, has its this keyword set to the provided value...

Reflect.get(target, key).bind(target)

这是exactly the same as你在这个.bind()中的用法:

target[key].bind(target)

The static Reflect.get() method works like getting a property from an object (target[propertyKey]) as a function.

底线

在这两种情况下(Reflect.get().bind()),上下文都转移到创建私有成员的对象。这在许多用例中都是必需的,并且与 Proxy 无关。

使用代理的工作解决方案

class Thing {
  #value
  constructor(value) { this.#value = value }
  value() { return this.#value }
  get value() { return this.#value; }
  set value(v) { this.#value = v; }
  someMethod() { return 'Cannot get here when proxied.'}
}

const thing = new Thing('foo')
const revokeMe = Proxy.revocable(thing, {
  get: (target, key) => {
    if (key === 'value') {
      return () => 'value is undefined (blocked by proxy)'
    }
    if(key === 'someMethod') {
      return () => `cannot invoke ${key}. (blocked by proxy)`;
    }
    return Reflect.get(target, key).bind(target);
  },
  set: (target, key, value) => {
    if (key === 'value') {
      console.log(`cannot set ${key} property. (blocked by proxy)`);
    }
    return Reflect.set(target, key, value);
  }
});
const proxy = revokeMe.proxy;
console.log(proxy.value());
proxy.value = 'test';
console.log(proxy.value());
console.log(proxy.someMethod());
revokeMe.revoke();
try {
  proxy.value();
} catch (err) {
  console.log('proxy has been revoked');
}
thing.value = 'new value';
console.log(thing.value);
console.log(thing.someMethod());

使用简单继承的可行解决方案

关注这个问题陈述:“暂时使变异方法和设置器无效,然后 [...] 恢复正常行为。

考虑到您提供的代码,该解决方案根本不需要代理。只需设置相关对象的原型并根据需要覆盖 properties/methods。

class Thing {
  #value
  constructor(value){ this.#value = value + ' cannot get here'}
  value(){ return this.#value + ' not gonna happen'}
  get value(){ return this.#value + ' not gonna happen'}
  set value(v) { this.#value = value;};
  toBeDisabled() { return 'will not show';}
}

class Overrides {
  constructor(value) {
    this.value = value + ' (set in Overrides)';
  }
  get value() {return 'value is undefined (set in Overrides)';}
  set value(v) {}
  toBeDisabled(){
    return 'NoOp (in Overrides)';
  }
}

let thing = new Thing('foo');
thing.__proto__ = new Overrides(thing.value);

thing.value = 'new value';
console.log(thing.value);
console.log(thing.toBeDisabled())
thing.__proto__ = {}
thing.value = 'now value will set; proxy is disabled;';
console.log(thing.value);