Как расширить класс, завернутый в прокси - PullRequest
1 голос
/ 31 марта 2020

У меня есть сложный класс, который требует, чтобы определенные аргументы передавались в конструктор. Однако я предоставляю клиентам упрощенный API.

Мой внутренний класс выглядит примерно так:

class Foo {
  constructor(p, c) {
  }

  foo() {
  }
}

Где p - это внутренняя ссылка, которая недоступна для клиентов .

Поддержка API Publi c

Я хочу разрешить клиентам создавать экземпляры этого класса, но я не хочу, чтобы им требовалась ссылка на частный объект p , Для пользователей этого API доступ к p будет трудоемким и нарушит существующий код, поэтому я хочу скрыть его с псевдонимом.

Подклассы для спасения? Почти.

Сначала я просто расширил Foo, спрятал закрытый аргумент (предоставив код для доступа к нему) и выставил его через publi c API:

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

This почти сработало, но я столкнулся с серьезным недостатком. Существуют ситуации, когда клиенту необходимо проверить тип объекта. В зависимости от ситуации Foo может быть создан внутренне с использованием внутреннего класса или клиентом с использованием API publi c.

Если для создания экземпляра Foo использовался API publi c, то внутренний instaceof проверки работают нормально: publicFoo instanceof Foo возвращает true. Но если API создал экземпляр Foo, используя внутренний класс, то публичные c instanceof проверки не пройдены: internalFoo instanceof PublicFoo возвращает false. Клиент может печатать экземпляры чеков, созданные с использованием API publi c, но проверки того же типа не выполняются для экземпляров, созданных внутри (например, с помощью заводских функций).

Этого следует ожидать и имеет смысл для меня , но это ломает мой вариант использования. Я не могу использовать простой подкласс, потому что подкласс не является надежным псевдонимом для внутреннего класса.

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!!!

Но Прокси нарушает наследование ...

Disaster! Клиенты больше не могут наследовать (свою версию) Foo!

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

  bar() {
  }
}

Ловушка конструктора Proxy всегда возвращает новый экземпляр Foo, , а не подкласс Bar.

Это приводит к ужасным, ужасным проблемам, таким как:

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

и

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

Я застрял.

Есть ли способ встретиться? все следующие критерии:

  • Мой publi c API должен неявно предоставлять "приватный" конструктор arg (но, по причинам ™ , я не могу сделать это в сам конструктор Foo ... это должно быть сделано через какую-то оболочку или перехват)
  • Версия Foo API publi c должна быть тонким псевдонимом Foo. Таким образом, учитывая f = new Foo(), тогда f instanceof PublicFoo должно вернуть true
  • PublicFoo все еще должен поддерживать extends. Итак, учитывая class Bar extends PublicFoo, тогда (new Bar()) instanceof Bar должен вернуть true.

Вот интерактивная демонстрация моей головоломки Proxy:

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

Ответы [ 2 ]

1 голос
/ 31 марта 2020

А как насчет прокси? Прокси нарушает наследование, поскольку он всегда возвращает новый экземпляр Foo, а не подкласс Bar.

Это потому, что ваша прокси-реализация всегда создавала new target, не учитывая newTarget параметр и передача его на 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);
  }
});

См. Также Что такое "new.target"? .

0 голосов
/ 31 марта 2020

Я бы порекомендовал не использовать подклассы или попробовать что-то умное с прокси. Почему бы просто не сделать

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), и в отношении instanceof PublicFoo и Foo полностью эквивалентны.

См. Также этот ответ для получения более подробной информации. .

...