如何扩展包裹在代理中的 class

How to extend a class wrapped in a Proxy

我有一个复杂的 class 需要将某些参数传递给构造函数。但是,我向客户公开了一个简化的 API。

我的内部 class 看起来像这样:

class Foo {
  constructor(p, c) {
  }

  foo() {
  }
}

其中 p 是客户不方便访问的内部参考。

支持 Public API

我想让客户创建此 class 的实例,但我不希望他们需要对私有 p 对象的引用。对于这个 API 的消费者,访问 p 会很费力并且会破坏现有代码,所以我想用别名隐藏它。

子class有救了?差不多。

起初我简单地扩展了 Foo,隐藏了私有参数(通过提供访问它的代码),然后通过 public API:

暴露了它
class PublicFoo extends Foo {
  constructor(c) {
    // Use internal functions to get "p"
    var p;
    super(p, c);
  }
}

这几乎成功了,但我 运行 陷入了一个重大缺陷。在某些情况下,客户需要测试对象的类型。根据情况,Foo 可能使用内部 class 在内部创建,或者由客户使用 public API.

如果 public API 用于创建 Foo 的实例,那么内部 instaceof 检查工作正常: publicFoo instanceof Foo returns true。但是,如果 API 使用内部 class 创建了一个 Foo 实例,那么 public instanceof 检查失败: internalFoo instanceof PublicFoo returns false。客户可以对使用 public API 创建的实例进行类型检查,但对于内部创建的实例(例如,通过工厂函数),相同类型的检查会失败。

这是意料之中的,对我来说很有意义,但它打破了我的用例。我不能使用简单的 sub-class 因为 sub-class 不是内部 class.

的可靠别名
var f = new Foo();
f instanceof PublicFoo; // false

代理呢?

所以我将“聪明”的装备提高了一个档次并尝试使用代理代替,似乎(著名的遗言) 求完美解决:

var PublicFoo = new Proxy(Foo, {
  construct(target, args) {
    // Use internal functions to get "p"
    var p;
    return new target(p, ...args);
  }
});

我可以公开代理、拦截对构造函数的调用、提供必要的私有对象引用并且 instanceof 没有损坏!

var f = new Foo();
f instanceof PublicFoo; // true!!!

但是 Proxy 破坏了继承...

灾难!客户不能再继承自(他们的)Foo!

class Bar extends PublicFoo {
  constructor(c) {
    super(c);
  }

  bar() {
  }
}

Proxy 的构造函数陷阱总是 return 是 Foo 的新实例,不是子class Bar

这会导致非常可怕的问题,例如:

(new Bar()) instanceof Bar; // false!!!! 

var b = new Bar();
b.bar() // Uncaught TypeError: b.bar is not a function

我卡住了。

有没有办法满足以下所有条件:

这是我的代理难题的交互式演示:

class Foo {
  constructor(p, c) {
    this.p = p;
    this.c = c;
  }
  
  foo() {
    return "foo";
  }
}

var PublicFoo = new Proxy(Foo, {
  construct(target, args) {
    var p = "private";
    return new target(p, ...args);
  }
});

var foo = new Foo("private", "public");
console.assert( foo instanceof PublicFoo, "Foo instances are also instances of PublicFoo" );
console.assert( foo.p === "private" );
console.assert( foo.c === "public" );

var publicFoo = new PublicFoo("public");
console.assert( publicFoo instanceof Foo, "PublicFoo instances are also instances of Foo" );
console.assert( publicFoo.p === "private" );
console.assert( publicFoo.c === "public" );

class Bar extends PublicFoo {
  constructor(c) {
    super(c);
  }
  
  bar() {
    return "bar";
  }
}

var i = new Bar("public");
console.assert( i instanceof Bar, "new Bar() should return an instance of Bar" );
console.assert( i.p === "private" );
console.assert( i.c === "public" );
i.foo(); // "foo"
i.bar(); // Uncaught TypeError: i.bar is not a function

我建议不要使用子类化,或者使用代理尝试一些巧妙的方法。为什么不直接做

class Foo {
  // private API - people can try but they won't come up with a valid `p`
  constructor(p, c) {
  }
  // public API
  static create(c, ...args) {
    const p = …; // Use internal functions to get "p"
    return new this(p, c, ...args);
  }
  // public API
  foo() {
  }
}

当然,人们不会使用 new Foo,而是 Foo.create(),他们的子类将不得不通过内部 p.

如果你绝对需要支持new PublicFoo语法,我会推荐

function PublicFoo(c) {
  const p = …; // Use internal functions to get "p"
  return new Foo(p, c, ...args);
}
PublicFoo.prototype = Foo.prototype;

(可能 Foo.prototype.constructor = PublicFoo,如果这对您很重要的话)。这种模式仍然很好地支持 ES6 继承(class extends PublicFoo),关于 instanceofPublicFooFoo 是完全等价的。

有关详细信息,另请参阅 this answer

What about a Proxy? A proxy breaks inheritance as it always returns a new instance of Foo, not the subclass Bar.

那是因为您的代理实现总是构造一个 new target,而不考虑 newTarget parameter 并将其传递给 Reflect.construct:

const PublicFoo = new Proxy(Foo, {
  construct(Foo, args, newTarget) {
    const p = …; // Use internal functions to get "p"
    return Reflect.construct(Foo, [p, ...args], newTarget);
  }
});

另见