为什么 Proxy 会打破这个绑定? 第一个例子第二个例子使用代理的工作解决方案使用简单继承的工作解决方案

问题描述

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

我很好奇 Javascript 代理如何以及为什么会破坏其代理目标的 this 绑定。在下面的示例中,我有一个简单的类,它在构造时摄取一个值,将其存储为私有字段,并在访问属性时返回它。请注意:

  1. 在没有处理程序的情况下尝试通过代理访问属性会引发错误
  2. 通过 get 显式转发处理程序 Reflect.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. 可以this 陷阱中显式绑定 get
  4. 但这并不是恢复预期行为

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,key)
})

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

// Explicitly binding the returned method *does* work
const proxy3 = new Proxy(thing1,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. 对于您提供的用例和人为代码,不需要代理,因为可以使用简单的继承来实现目标(最底层的解决方案)

第一个例子

第一个示例中的无操作代理没有损坏。 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

bind() 方法创建了一个新函数,当被调用时,它有它的 this 关键字设置为提供的值...

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

exactly the same as 您在这方面使用的 .bind() 是什么:

target[key].bind(target)

静态 Reflect.get() 方法的工作原理类似于从 对象 (target[propertyKey]) 作为函数。

底线

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

使用代理的工作解决方案

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,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());

使用简单继承的工作解决方案

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

鉴于您提供的代码,该解决方案根本不需要代理。只需设置相关对象的原型并根据需要覆盖属性/方法。

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);