ts: установить метод в одном классе, равный методу в другом классе - PullRequest
0 голосов
/ 18 июня 2020

Почему это не работает? ... и как это правильно сделать?

class A {
    constructor() {
        console.log('constructin A')
    }
  public someMethod = (x: string) => {
    console.log(x)
    }
}

class B {
    private myA: A
    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }
    // Fails with "Uncaught TypeError: Cannot read property 'someMethod' of undefined"
    anotherMethod = this.myA.someMethod;
}

const a = new A()
const b = new B(a)

b.anotherMethod('hello')

машинописная ссылка на игровую площадку

Примечания:

  • Это not typescript specifici c, я получаю ту же ошибку в эквивалентном обычном js.
  • Причина, по которой я хочу использовать этот синтаксис anotherMethod = this.myA.someMethod вместо anotherMethod = (x: string) => { this.myA.someMethod(x) }, заключается в том, что в моем реальном мире вариант использования someMethod имеет сложные аннотации типов, которые я бы предпочел не дублировать на anotherMethod

EDIT (19.06.2020) - «с развязкой»

Предлагаемые ответы и несколько комментариев, например, это , объясняют почему мой синтаксис не работает. Что касается решений:

Разделение A и B : я не указывал это в моем исходном вопросе / примере, но причина, по которой я хочу B, используйте метод в A, это то, что я хотел бы разъединить два. В моем реальном случае использования у меня есть несколько A, все они соответствуют одному и тому же интерфейсу с someMethod, но с разными его реализациями. Я хочу, чтобы B соответствовал c деталям реализации, и просто использовал someMethod все, что предоставляет c A. принятый ответ подобрал это и отлично справляется с конкретизацией его в примере кода. Он очень хорошо объясняет недостатки различных решений и предоставляет (imho) лучший синтаксис без ошибок для моего варианта использования. Если вы торопитесь, ознакомьтесь с принятым ответом для этой строки:

public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
    return this.myA.someMethod(...args);
}

Если вам не нужна развязка , этот ответ может быть всем, что вам нужно нужно.

Ответы [ 2 ]

3 голосов
/ 18 июня 2020

anotherMethod будет определено до того, как myA будет установлено в конструкторе. Возможное решение вашей проблемы - использовать геттер:

get anotherMethod() {
    return this.myA.someMethod;
}

Playground

Чтобы добавить к этому, вы можете сделать метод в A stati c. Таким образом, вам вообще не нужно передавать экземпляр A в B. Просто убедитесь, что B имеет доступ к A.

class A {
    constructor() {
        console.log('constructin A')
    }

    static someMethod = (x: string) => {
        console.log(x)
    }
}

class B {
    private myA: A
    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }

    anotherMethod = A.someMethod;
}

const a = new A()
const b = new B(a)

b.anotherMethod("Hello")

Детская площадка

1 голос
/ 18 июня 2020

Причина возникновения проблемы:

Проблема в том, что anotherMethod = this.myA.someMethod; выполняется перед this.myA = a;. Вот код JavaScript, который генерирует компилятор TypeScript:

class B {
    constructor(a) {
        this.anotherMethod = this.myA.someMethod;
        console.log('constructin B');
        this.myA = a;
    }
}

Итак, это not не может вызвать ошибку.

Если вы нужно прикрепить метод во время строительства, вам нужно сделать это самостоятельно в конструкторе:

class A {
    constructor() {
        console.log('constructin A')
    }
  public someMethod = (x: string) => {
    console.log(x)
    }
}

class B {
    private myA: A

    //declare the anotherMethod interface to match the  someMethod interface
    public anotherMethod: typeof A.prototype.someMethod

    constructor(a: A) {
        console.log('constructin B')
        this.myA = a

        //assign the method. Note: `this.anotherMethod = a.someMethod` is equivalent
        this.anotherMethod = this.myA.someMethod;
    }
}

const a = new A()
const b = new B(a)

b.anotherMethod('hello') //OK
b.anotherMethod(42)      //error - does not accept numbers

Playground Link

А вот как это может выглядеть в чистый JavaScript

class A {
  constructor() {
    console.log('constructin A')
  }
  someMethod = (x) => {
    console.log(x)
  }
}

class B {
  constructor(a) {
    console.log('constructin B')
    this.myA = a;
    this.anotherMethod = this.myA.someMethod;
  }
}

const a = new A()
const b = new B(a)

b.anotherMethod('hello')

Лучшее решение

В комментариях задающий вопрос сказал:

Я хотел разделить проблемы, чтобы реализация детали вызова api делегируются классам A, а B просто состоит из спецификаций c A.

Для такого случая использования я бы предложил не напрямую присоединять методы, а вместо этого просто объявлять один метод в B, который делегирует все вызовы myA:

//method with the same signature as A.someMethod
public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
    //use the delegate for the call
    return this.myA.someMethod(...args);
}

Это намного чище, чем назначение методов экземплярам в время строительства. ...args: Parameters<A['someMethod']> всегда будет равно параметрам, которые принимает someMethod. И наоборот, ReturnType<A['someMethod']> преобразуется в тип, возвращаемый someMethod. Если это void, значит, он все еще действителен.

Вот как это может выглядеть - я превратил A в интерфейс, который реализуется разными классами. B теперь знает только об интерфейсе и не заботится о том, как он реализован:

interface A {
    someMethod(x: string): void;
}

class X implements A {
    constructor() {
        console.log('constructin X')
    }
  public someMethod = (str: string) => {
      console.log("X", str);
    }
}

class Y implements A {
    constructor() {
        console.log('constructin Y')
    }
  public someMethod = (str: string) => {
      console.log("Y", str);
    }
}
class Z implements A {
    constructor() {
        console.log('constructin Z')
    }
  public someMethod = (str: string) => {
      console.log("Z", str);
    }
}

class B {
    private myA: A

    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }

    public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
        return this.myA.someMethod(...args);
    }
}

const x = new X()
const y = new Y()
const z = new Z()
const b1 = new B(x)
const b2 = new B(y)
const b3 = new B(z)

b1.anotherMethod('hello') //OK
b2.anotherMethod('hello') //OK
b3.anotherMethod('hello') //OK

b1.anotherMethod(42)      //error - does not accept numbers

Playground Link

Здесь можно прекратить читать.


(Необязательно дочитать до конца) Почему я предлагаю это решение

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

get anotherMethod() {
    return this.myA.someMethod;
}

В приведенном выше случае getter всегда будет возвращать ссылку на функцию . Это позволяет вызывать

b.anotherMethod("hello"), и это будет правильно скомпилироваться, будет иметь правильную сигнатуру метода и может быть вызвано. Однако есть тонкая, но очень часто встречающаяся проблема: контекст this будет потерян . Рекомендуемая литература:

Как работает ключевое слово "this"?

Вот краткая версия - значение для this определяется на момент вызов функции. Чтобы не усложнять, воспользуйтесь практическим правилом: значение всегда равно перед последней точкой при вызове:

 //calling quux()
 foo.bar.baz.quux() // -> this = foo.bar.baz
 ^^^^^^^^^^^                     ^^^^^^^^^^^
      |                               |
      ---------------------------------
 //calling baz()
 foo.bar.baz() // -> this = foo.bar
 ^^^^^^^                    ^^^^^^^
    |                          |
    ----------------------------
 //calling bar()
 foo.bar() // -> this = foo
 ^^^                    ^^^
  |                      |
  ------------------------
 //calling foo()
 foo() // -> this = undefined
^
|
nothing

информации, я предлагаю прочитать ссылки, которые я предоставил.

Это важно, потому что вызов определяет this. Очень распространенная проблема в JavaScript - потеря контекста при выполнении следующих действий:

"use strict";

const foo = {
  value: 42,
  bar() {
    return this.value;
  }
}

console.log(foo.bar()); //this = foo; foo.value = 42

//get a function reference
const func = foo.bar;

console.log(func()); // this = undefined; undefined.value = error

Это самая основная c форма ошибки. Он может проявляться разными способами, например, при передаче обратных вызовов:

functionThatTakesCallback(foo.bar);

, и он будет проявляться при использовании get. На самом деле это более коварно, поскольку вы вызовете функцию, но с новым контекстом:

class B {
    private myA: A
    /* simplified for brevity */
    get anotherMethod() {
        return this.myA.someMethod;
    }
}

const b = new B(a);
b.anotherMethod("hello"); // this = b;

Теперь невозможно гарантировать, что anotherMerhod (который является псевдонимом для someMethod) будет работать правильно.

  1. Если someMethod не использует никаких данных экземпляра (нет this внутри него), тогда он будет работать. Но нет возможности проверить это.

  2. Если someMethod является обычным методом (или функцией - разница здесь незначительна) и использует this, то вызов b.anotherMethod("hello") будет почти гарантированно даст неверный результат или даже ошибку.

  3. Если someMethod является «стрелочным методом» (будет go подробнее ниже), то контекст this будет автоматически привязан и, следовательно, результат будет правильным. Опять же, нет удобного способа проверить это.

Что такое «метод стрелок»? Это такая конструкция:

class X implements A {
    private myProp: string;
    /* simplified for brevity */
    public someMethod = (str: string) => { //"arrow method"
        console.log(this.myProp, str);
    }
}

На самом деле это функция стрелки, назначенная как свойство класса. Разница в том, что он автоматически добавляется к экземпляру, и this будет лексически привязан внутри, поэтому он всегда будет указывать на текущий экземпляр.

Напротив, обычный метод выглядит так:

class Y implements A {
    private myProp: string;
    /* simplified for brevity */
    public someMethod(str: string) { //regular method
        console.log(this.myProp, str);
    }
}

Он используется всеми экземплярами в том виде, в каком он существует на прототипе. Это гарантирует, что в памяти будет только одна копия этой функции, свойство класса функции стрелки создает по одной для каждого созданного экземпляра X. Это не должно быть проблемой, но если будет создано много X объектов, это приведет к увеличению использования памяти. Недостатком Y является потенциальная потеря this. Итак, это своего рода балансирующий акт.

Вот пример того, как go может ошибаться:

interface A {
    someMethod(x: string): void;
}

class X implements A {
    //instance property
    private myProp: string;

    constructor(name: string) {
        console.log('constructin X')

        this.myProp = name;
    }
    public someMethod = (str: string) => { //"arrow method"
        console.log(this.myProp, str);
    }
}

class Y implements A {
    //instance property
    private myProp: string;

    constructor(name: string) {
        console.log('constructin Y')

        this.myProp = name;
    }
    public someMethod(str: string) { //regular method
        console.log(this.myProp, str);
    }
}
class B {
    private myA: A

    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }

    get anotherMethod() {
        return this.myA.someMethod;
    }
}

const x = new X("Foo")
const y = new Y("Bar")
const b1 = new B(x)
const b2 = new B(y)

b1.anotherMethod('hello'); //Foo hello
b2.anotherMethod('hello'); //undefined hello

//this.myProp is taken from B. Illustration:
(b2 as any).myProp = "This is B"
b2.anotherMethod('hello')  //This is B hello

Playground Link

С учетом всего вышесказанного, есть способ исправить подход get. Значение this может быть постоянно установлено с помощью Function#bind

class B {
    private myA: A
    /* simplified for brevity */
    get anotherMethod() {
        return this.myA.someMethod.bind(this.myA);
    }
}

Проблема теперь в том, что .bind() каждый раз возвращает новую функцию. Таким образом,

b.anotherMethod("hello");
b.anotherMethod("hello");

будет иметь правильное значение для this, но это фактически создает две функции, выполняет их, а затем отбрасывает их. В этом случае память не является проблемой, поскольку функции будут просто собирать мусор, но сама сборка мусора может быть проблемой. Код, который вызывает геттер много раз, например, такой:

for(let i = 0; i < 9000; i++) {
  b.anotherMethod("hello");
}

, приведет к множеству запусков сборщика мусора, что может снизить производительность. Опять же, не обязательно проблема, а кое-что, о чем нужно упомянуть.

Другая потенциальная проблема - если вам когда-нибудь понадобится сравнить функции на равенство

b.anotherMethod === b.anotherMethod //false

Ссылка на игровую площадку

Каждый b.anotherMethod выполняет разные функции. Редко, когда вам нужно будет сравнивать функции, но это может возникнуть, если вы используете Set или Map, например:

const set = new Set();
set.add(b.anotherMethod);

//later

set.has(b.anotherMethod); //false

Я просто упомяну еще более сложный способ убедиться, что работа get сохраняет

get anotherMethod() {
    return this.myA.someMethod;
}

, а затем использует прокси для обертывания X экземпляров и перехвата любых вызовов anotherMethod и вместо этого выполняет Function.call или Function#appy для сохранения контекста.

Однако, на мой взгляд, все эти решения имеют собственные проблемы. В частности, подход Proxy очень сложен. Вот почему я предпочитаю простой метод, который делегирует вызов this.myA:

public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
    return this.myA.someMethod(...args);
}
  • , его легко читать - вы точно знаете, что делается.
  • легко понять - намерение ясно
  • нет скрытых недостатков:
    • this.myA.someMethod(...args) всегда будет использовать this.myA как значение this для someMethod независимо от того, как этот метод определен .
    • это обычный метод, поэтому в памяти есть только одна его копия.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...