【问题标题】:Why does Proxy break this-binding?为什么代理会破坏这个绑定?
【发布时间】:2021-05-27 05:14:58
【问题描述】:

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

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

  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陷阱中显式绑定this
  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, {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)
  }
}

【问题讨论】:

  • 您想要解决这个问题:“尝试代理一组对象”还是只是好奇为什么一个对象不能访问另一个对象的私有属性? Proxy 对象,即使是空实现仍然是一个对象。无论如何,我有一个解决方案可以让你实现你的目标(但与你想象的不同)。
  • 哈哈我很感兴趣!那就继续吧……

标签: javascript javascript-proxy


【解决方案1】:

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

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

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

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

target[key].bind(target)

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

底线

在这两种情况下(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());

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-08
    • 2016-01-19
    • 2016-08-16
    • 1970-01-01
    • 2021-08-15
    • 2011-07-23
    • 2013-01-09
    • 1970-01-01
    相关资源
    最近更新 更多