метод дочернего расширенного класса вызывает его супер версию, но он все еще видит только дочерние данные - PullRequest
1 голос
/ 14 апреля 2019

класс B расширяет класс A. Я назову A родителем, а B дочерним. У обоих есть конструкторы. B вызывает super () внутри своего конструктора. Оба имеют метод с одинаковым именем. Возможно, по стечению обстоятельств или по ошибке оба имеют переменную this.x. Тогда нет возможности получить доступ к родительской переменной this.x. Затем он становится точкой, возможно, непреднамеренного общения между ребенком и родителем.

   class A {
      constructor(){
        this.x = "super x!";
      }
      logx(){
        console.log(this.x);
      }
    }

    class B extends A{
      constructor(){
        super();
        this.x = "derived x.";
      }
      logx(){
        super.logx();
      }
    }

    let b = new B;
    b.logx();  // expected "super x!", but it prints "derived x".

Может случиться так, что класс А происходит из библиотеки или был написан кем-то другим. Может даже случиться так, что автор класса A придет, отредактирует код и добавит новую переменную, которая затем псевдонимом для ребенка, которого он даже не знает, существует. Затем автор дочернего класса должен стать заядлым читателем изменений в родительском классе, чтобы он или она могли соответствующим образом обновить свой собственный код, если даже этот автор все еще находится в проекте. (Это такая ошибка, которая привела меня сюда сегодня, это ее перегонка.)

В следующем коде я предотвращаю эту проблему, давая каждой переменной префикс, совпадающий с именем класса. Тогда я получаю ожидаемое поведение. Конечно, есть лучший способ. Возможно, некоторые из этих частных / публичных ключевых слов помогут?

      constructor(){
        this.A_x = "super x!";
      }
      logx(){
        console.log(this.A_x);
      }
    }

    class B extends A{
      constructor(){
        super();
        this.B_x = "derived x.";
      }
      logx(){
        super.logx();
      }
    }

    let b = new B;
    b.logx();  // expected "super x!", and indeed it prints "super x!"

Это также происходит для вызовов методов, хотя это не так удивительно, потому что а) это считается «полиморфизмом»; б) обычно изменения в интерфейсе вышестоящего кода имеют последующие эффекты кода. Однако у программиста могут быть некоторые вспомогательные функции, не предназначенные для интерфейса, и если автору дочернего класса случается подумать об одном и том же имени вспомогательной функции, или расширяет интерфейс функцией с этим именем ...

   class A {
      constructor(){
        this.x = "super x!";
      }
      f(){
        console.log("I am a super f()!");
      }
      logx(){
        this.f(); // aliased - polymorphism behavior
        console.log(this.x);
      }
    }

   class B extends A{
      constructor(){
        super();
        this.x = "derived x.";
      }
      f(){
        console.log("I am a derived f()");
      }
      logx(){
        super.logx();
      }
    }

   let b = new B;
   b.logx();

вывод на консоль:

I am derived f()
derived x.

Согласно комментариям Джонаса Уилмса о его раскручивании происходящего, верно, что шаблон композиции может использоваться для инкапсуляции данных родителя и, таким образом, предотвращения случайного наложения псевдонимов:

   class A {
      constructor(){
        this.x = "super x!";
      }
      f(){
        console.log("I am a super f()!");
      }
      logx(){
        this.f();
        console.log(this.x);
      }
    }

    class B {
      constructor(){
        this.a = new A();
        this.x = "derived x.";
      }
      f(){
        console.log("I am a derived f()");
      }
      logx(){
        this.a.logx();
      }
    }

    let b = new B;
    b.logx();

И ведет себя как положено, вывод консоли:

    I am a super f()!
    super x!

Однако это не без проблем. Во-первых, оператор instanceof не работает. Во-вторых, мы не наследуем любые методы. Автору дочернего класса придется добавить заглушки, которые просто принимают аргументы и передают их методу родительского класса. Это может повлиять на производительность. См. ES6 и соавт. Можно ли определить метод всеобщего охвата? .

... кажется, этот вопрос сводится к тому, «как вы определяете, что на интерфейсе, а что нет?» ну и дела, есть демонстрация того, почему кто-то может захотеть это сделать.

1 Ответ

4 голосов
/ 14 апреля 2019

На самом деле ваша class иерархия равна

 // a constructor is just a function
 function A() {
  this.x = "super x!";
}

A.prototype.logx = function() { console.log(this.x); };

function B() {
  A.call(this); // "this" gets passed, no new instance gets created
  this.x = "derived x";
}

B.prototype = Object.create(A.prototype); // extending a class basically lets the prototype of the class inherit the prototype of the superclass
B.prototype.constructor = B;

B.prototype.logx = function() {
  A.prototype.logx.call(this); // we can reference A#logx as it exists on the prototype
};

// using "new" basically creates a new object inheriting the prototype, then executes the constructor on it
let b = Object.create(B.prototype);
B.call(b);

Таким образом, хотя на самом деле есть два logx метода, на которые вы можете ссылаться (один на прототипе А и один на B), есть только один экземпляр (this), который был передан во время построения, и установив свойство объект переопределяет предыдущее значение. Следовательно, вы правы, невозможно иметь разные свойства с одинаковым именем.

Боже, надеюсь, что не нужно что-то делать, например, принять соглашение о присвоении каждой переменной префикса на основе имени ее класса, если кто-то хочет убедиться, что родительские переменные остаются независимыми

Я действительно рекомендую использовать Typescript, чтобы следить за структурой (есть модификатор свойств private и readonly). В JS вы можете использовать символы для имитации частных свойств: 101

 class A {
   constructor() {
     this[A.x] = "stuff";
   }
 }

 A.x = Symbol();

 class B extends A {
   constructor() {
     this[B.x] = "other stuff";
   }
 }

 B.x = Symbol();

 console.log(new B()[A.x]);

(наверняка вы можете хранить символы в любой переменной, нет необходимости делать ее частью класса).

Или вы просто отказываетесь от наследства, и составляете B с помощью A:

 class B {
   constructor() {
     this.a = new A();
     this.x = "new x";
   }
 }

 (new B).x
 (new B).a.x

Это также произойдет для вызовов методов?

Да, поскольку цепочка наследования для экземпляра B:

 b -> B.prototype -> A.prototype

Метод будет сначала найден в b, затем в B и, наконец, в A, поэтому, если есть метод с именем "logx" как в A, так и в B, тот из B будет быть взятым Вы также можете сделать:

 b.logx = function() { console.log("I'm first!");

Итак, что делать при написании родительского кода, если он хочет, чтобы родительский f ()?

Вы можете напрямую вызвать его по прототипу:

 A.prototype.logx.call(b /*...arguments*/);

из метода вы можете взять this вместо конкретного экземпляра (b в этом примере). Если вы не хотите использовать конкретную реализацию, кроме класса super , используйте super.logx() так же, как вы это сделали.


¹ Если честно: у меня никогда не было проблем с этим, если вы правильно называете свои свойства, имена действительно редко сталкиваются.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...