Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript "OOP" and prototypes with multiple-level inheritance

I'm new to Javascript programming and I'm approaching my first application (a game, indeed) from an object oriented programming perspective (I know js is not really object oriented, but for this particular problem it was easier for me to start like this).

I have a hierarchy of "classes" where the top-most ("Thing" class) defines a list of related things (attached items in the game). It's inherited by a ThingA class which is inherited by ThingA1 and ThingA2 classes.

The minimal example would be:

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');

At the end of the code, when the relateThing is executed, every ThingA, ThingA1 and ThingA2 is going to execute it (not the last "Thing" object in the array). I've found if I define the relateThing function in the ThingA prototype, it will work right. Because of how the game is designed I'll prefer not to have to do that.

Maybe I'm not understanding something about how the prototypes work in javascript. I know the function is shared among all the objects, but i guess the execution would be individual. Could somebody explain why is this happening and how to solve it? I don't know if I'm doing the inheritance wrong, or the prototypes definitions, or what.

Thanks in advance.

like image 382
neverbot Avatar asked Feb 23 '13 13:02

neverbot


People also ask

Does JavaScript support multi level inheritance?

An object can be inherited from multiple other objects, i.e. the object has common property from other parent objects not only a single parent. In JavaScript, this can be achieved by merging properties from different properties into one single object.

What is prototype and prototype inheritance in JavaScript?

When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype.

What is multi level inheritance in OOP?

The multi-level inheritance includes the involvement of at least two or more than two classes. One class inherits the features from a parent class and the newly created sub-class becomes the base class for another new class.

Does Oops support multiple inheritance?

Details. In object-oriented programming (OOP), inheritance describes a relationship between two classes in which one class (the child class) subclasses the parent class. The child inherits methods and attributes of the parent, allowing for shared functionality.


2 Answers

Welcome to the prototype chain!

Let's see what it looks like in your example.

The Problem

When you call new Thing(), you are creating a new object with a property relatedThings which refers to an array. So we can say we have this:

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

You are then assigning this instance to ThingA.prototype:

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

So each instance of ThingA will inherit from the Thing instance. Now you are going to create ThingA1 and ThingA2 and assign a new ThingA instance to each of their prototypes, and later create instances of ThingA1 and ThingA2 (and ThingA and Thing, but not shown here).

The relationship is now this (__proto__ is an internal property, connecting an object with its prototype):

                               +-------------+
                               |   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__  |-----------+
+-------------+                        

And because of that, every instance of ThingA, ThingA1 or ThingA2 refers to one and the same array instance.

This is not what you want!


The Solution

To solve this problem, each instance of any "subclass" should have its own relatedThings property. You can achieve this by calling the parent constructor in each child constructor, similar to calling super() in other languages:

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

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

// ...

This calls Thing and ThingA and sets this inside those function to the first argument you pass to .call. Learn more about .call [MDN] and this [MDN].

This alone will change the above picture to:

                               +-------------+
                               |   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__  |-----------+
+-------------+

As you can see, each instance has its own relatedThings property, which refers to a different array instance. There are still relatedThings properties in the prototype chain, but they are all shadowed by the instance property.


Better Inheritance

Also, don't set the prototype with:

ThingA.prototype = new Thing();

You actually don't want to create a new Thing instance here. What would happen if Thing expected arguments? Which one would you pass? What if calling the Thing constructor has side effects?

What you actually want is to hook up Thing.prototype into the prototype chain. You can do this with Object.create [MDN]:

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

Anything that happens when the constructor (Thing) is executed will happen later, when we actually create a new ThingA instance (by calling Thing.call(this) as shown above).

like image 77
Felix Kling Avatar answered Oct 18 '22 23:10

Felix Kling


If you don't like the way prototyping works in JavaScript in order to achieve a simple way of inheritance and OOP, I'd suggest taking a look at this: https://github.com/haroldiedema/joii

It basically allows you to do the following (and more):

// 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
like image 27
Harold Avatar answered Oct 18 '22 22:10

Harold