Javascript "ООП" и прототипы с многоуровневым наследованием

Я новичок в программировании Javascript, и я приближаюсь к своему первому приложению (игра, действительно) с точки зрения объектно-ориентированного программирования (я знаю, что js не является объектно-ориентированным, но для этой конкретной проблемы мне было легче чтобы начать так).

У меня есть иерархия "классов", где самый верхний (класс "Thing" ) определяет список связанных вещей (прикрепленные элементы в игре). Он унаследован классом ThingA, который наследуется классами ThingA1 и ThingA2.

Минимальным примером может быть:

function Thing() 
{
  this.relatedThings   = [];
}
Thing.prototype.relateThing = function(what)
{
  this.relatedThings.push(what);
}

ThingA.prototype = new Thing();
ThingA.prototype.constructor = ThingA;
function ThingA()
{
}

ThingA1.prototype = new ThingA();
ThingA1.prototype.constructor = ThingA1;
function ThingA1()
{

}

ThingA2.prototype = new ThingA();
ThingA2.prototype.constructor = ThingA2;
function ThingA2()
{    
}

var thingList = [];

thingList.push(new ThingA());
thingList.push(new ThingA1());
thingList.push(new ThingA2());
thingList.push(new ThingA2());
thingList.push(new Thing());

thingList[1].relateThing('hello');

В конце кода, когда выполняется relateThing, каждый ThingA, ThingA1 и ThingA2 собираются выполнить его (а не последний объект Thing в массиве). Я нашел, если я определяю функцию relateThing в прототипе ThingA, он будет работать правильно. Из-за того, как разрабатывается игра, я предпочел бы не делать этого.

Возможно, я не понимаю, как работают прототипы в javascript. Я знаю, что функция распределяется между всеми объектами, но я предполагаю, что выполнение будет индивидуальным. Может ли кто-нибудь объяснить, почему это происходит и как его решить? Я не знаю, ошибаюсь ли я наследование, или определения прототипов, или что.

Спасибо заранее.

Ответ 1

Добро пожаловать в цепочку прототипов!

Посмотрите, как он выглядит в вашем примере.

Проблема

Когда вы вызываете new Thing(), вы создаете новый объект со свойством relatedThings, который ссылается на массив. Поэтому мы можем сказать, что у нас есть это:

+--------------+
|Thing instance|
|              |
| relatedThings|----> Array
+--------------+     

Затем вы назначаете этот экземпляр ThingA.prototype:

+--------------+
|    ThingA    |      +--------------+
|              |      |Thing instance|
|   prototype  |----> |              |
+--------------+      | relatedThings|----> Array
                      +--------------+

Таким образом, каждый экземпляр ThingA будет наследоваться от экземпляра Thing. Теперь вы создадите ThingA1 и ThingA2 и назначьте новый экземпляр ThingA каждому из своих прототипов, а затем создадите экземпляры ThingA1 и ThingA2ThingA и Thing, но не показано здесь).

Теперь отношение это (__proto__ является внутренним свойством, соединяющим объект с его прототипом):

                               +-------------+
                               |   ThingA    |
                               |             |    
+-------------+                |  prototype  |----+
|   ThingA1   |                +-------------+    |
|             |                                   |
|  prototype  |---> +--------------+              |
+-------------+     |    ThingA    |              |
                    | instance (1) |              |
                    |              |              |
+-------------+     |  __proto__   |--------------+ 
|   ThingA1   |     +--------------+              |
|   instance  |           ^                       |
|             |           |                       v
|  __proto__  |-----------+                 +--------------+
+-------------+                             |Thing instance|
                                            |              |
                                            | relatedThings|---> Array
+-------------+     +--------------+        +--------------+ 
|   ThingA2   |     |   ThingA     |              ^
|             |     | instance (2) |              |
|  prototype  |---> |              |              |
+-------------+     |  __proto__   |--------------+
                    +--------------+
+-------------+           ^
|   ThingA2   |           |  
|   instance  |           |
|             |           |
|  __proto__  |-----------+
+-------------+                        

И из-за этого каждый экземпляр ThingA, ThingA1 или ThingA2 относится к одному и тому же экземпляру массива.

Это не, что вы хотите!


Решение

Чтобы решить эту проблему, каждый экземпляр любого "подкласса" должен иметь свое собственное свойство relatedThings. Вы можете добиться этого, вызвав родительский конструктор в каждом дочернем конструкторе, похожий на вызов super() на других языках:

function ThingA() {
    Thing.call(this);
}

function ThingA1() {
    ThingA.call(this);
}

// ...

Это вызывает Thing и ThingA и устанавливает this внутри этой функции на первый аргумент, который вы передаете на .call. Подробнее о .call [MDN] и this [MDN].

Только это изменит изображение выше:

                               +-------------+
                               |   ThingA    |
                               |             |    
+-------------+                |  prototype  |----+
|   ThingA1   |                +-------------+    |
|             |                                   |
|  prototype  |---> +--------------+              |
+-------------+     |    ThingA    |              |
                    | instance (1) |              |
                    |              |              |
                    | relatedThings|---> Array    |
+-------------+     |  __proto__   |--------------+ 
|   ThingA1   |     +--------------+              |
|   instance  |           ^                       |
|             |           |                       |
|relatedThings|---> Array |                       v
|  __proto__  |-----------+                 +--------------+
+-------------+                             |Thing instance|
                                            |              |
                                            | relatedThings|---> Array
+-------------+     +--------------+        +--------------+ 
|   ThingA2   |     |   ThingA     |              ^
|             |     | instance (2) |              |
|  prototype  |---> |              |              |
+-------------+     | relatedThings|---> Array    |
                    |  __proto__   |--------------+
                    +--------------+
+-------------+           ^
|   ThingA2   |           |  
|   instance  |           |
|             |           |
|relatedThings|---> Array | 
|  __proto__  |-----------+
+-------------+

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


Лучшее наследование

Кроме того, не устанавливайте прототип с помощью:

ThingA.prototype = new Thing();

На самом деле вы не хотите создавать новый экземпляр Thing здесь. Что произойдет, если Thing ожидаемые аргументы? Какой из них вы пройдете? Что делать, если вызов конструктора Thing имеет побочные эффекты?

На самом деле вы хотите подключить Thing.prototype к цепочке прототипов. Вы можете сделать это с помощью Object.create [MDN]:

ThingA.prototype = Object.create(Thing.prototype);

Все, что происходит при выполнении конструктора (Thing), произойдет позже, когда мы фактически создадим новый экземпляр ThingA (вызывая Thing.call(this), как показано выше).

Ответ 2

Если вам не нравится, как прототипирование работает в JavaScript, чтобы достичь простого способа наследования и ООП, я бы предложил взглянуть на это: https://github.com/haroldiedema/joii

В основном это позволяет вам сделать следующее (и более):

// First (bottom level)
var Person = new Class(function() {
    this.name = "Unknown Person";
});

// Employee, extend on Person & apply the Role property.
var Employee = new Class({ extends: Person }, function() {
    this.name = 'Unknown Employee';
    this.role = 'Employee';
});

// 3rd level, extend on Employee. Modify existing properties.
var Manager = new Class({ extends: Employee }, function() {

    // Overwrite the value of 'role'.
    this.role = 'Manager';

    // Class constructor to apply the given 'name' value.
    this.__construct = function(name) {
        this.name = name;
    }
});

// And to use the final result:
var myManager = new Manager("John Smith");
console.log( myManager.name ); // John Smith
console.log( myManager.role ); // Manager

Ответ 3

В OOP, когда вы определяете конструктор подкласса, вы также (неявно или явно) выбираете конструктор из супер-типа. Когда строится вспомогательный объект, оба конструктора выполняются, сначала из суперкласса, а все остальные - по иерархии.

В javascript это должно быть явно вызвано и не приходит автоматически!

function Thing() {
  this.relatedThings = [];
}
Thing.prototype.relateThing = function(what){
  this.relatedThings.push(what);
}

function ThingA(){
  Thing.call(this);
}
ThingA.prototype = new Thing();

function ThingA1(){
  ThingA.call(this);
}
ThingA1.prototype = new ThingA();

function ThingA2(){
  ThingA.call(this);

}
ThingA2.prototype = new ThingA();

Если вы этого не сделаете, весь экземпляр ThingA, ThingA1 и ThingA2 содержит один и тот же родственный массив Things, построенный в конструкторе Thing и вызываемый один раз для всех экземпляров в строке:

ThingA.prototype = new Thing();

В глобальном масштабе, infact, в вашем коде у вас есть только 2 вызова конструктора Thing, и этот результат состоит всего из двух экземпляров связанного массива.