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

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

У меня есть иерархия «классов», где самый верхний (класс «Вещь») определяет список связанных вещей (прикрепленных элементов в игре). Он наследуется классом 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');

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

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

Заранее спасибо.


person neverbot    schedule 23.02.2013    source источник
comment
JavaScript в значительной степени основан на объектах, так почему бы ему не быть объектно-ориентированным?   -  person Felix Kling    schedule 23.02.2013
comment
Насколько я знаю о javascript, прототипные языки не совсем такие же, как объектно-ориентированные языки (нет классов и т. д.), хотя концепция может быть похожей. Но я не эксперт.   -  person neverbot    schedule 23.02.2013
comment
Тот факт, что JavaScript строго объектно-ориентирован, не означает, что он работает так же, как другие языки OO. На самом деле наследование плохо поддерживается. Многие библиотеки реализуют расширенные способы, которые на самом деле являются обходными путями, улучшающими работу наследования. Одна из лучших, которые я нашел в настоящее время, - это библиотека Джона Резига, которая основана на библиотеках Prototype и base2, но в дальнейшем улучшена. ejohn.org/blog/simple-javascript-inheritance   -  person marekful    schedule 23.02.2013
comment
Ваш код не работает должным образом? Не понял, какой результат вы хотели и что получили взамен   -  person Joel Blum    schedule 23.02.2013
comment
@Joel_Blum После выполнения все объекты имеют приветствие внутри соответствующего массива relatedThings. Я ожидал, что это будет только в thingList[1].   -  person neverbot    schedule 23.02.2013
comment
@neverbot: ООП не означает, что вы должны иметь классы, на основе прототипа также является ООП   -  person Bergi    schedule 24.02.2013


Ответы (3)


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

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

Эта проблема

Когда вы вызываете 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]< /em> и 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), как показано выше).

person Felix Kling    schedule 23.02.2013

Если вам не нравится, как прототипирование работает в 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
person Harold    schedule 22.01.2014

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

В 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 содержат один и тот же массив связанных вещей, созданный в конструкторе Thing и вызываемый один раз для всех экземпляров в строке:

ThingA.prototype = new Thing();

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

person Plap    schedule 23.02.2013
comment
@Felix Kling: Спасибо за совет, я исправил. Ваш тоже прочитал, поздравляю :) - person Plap; 23.02.2013