Причина возникновения проблемы:
Проблема в том, что 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
) будет работать правильно.
Если someMethod
не использует никаких данных экземпляра (нет this
внутри него), тогда он будет работать. Но нет возможности проверить это.
Если someMethod
является обычным методом (или функцией - разница здесь незначительна) и использует this
, то вызов b.anotherMethod("hello")
будет почти гарантированно даст неверный результат или даже ошибку.
Если 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
независимо от того, как этот метод определен . - это обычный метод, поэтому в памяти есть только одна его копия.