Monday, September 20, 2010

Javascript Prototype Inheritance

A common desire when coding in an OO way is to create a class that inherits from another class. So how do you do this in Javascript? You can find various links on the web about how you can do this. However, just because “you can program FORTRAN in any language," doesn't mean that I want to program Java in Javascript. So, to me, learning Javascript means learning the Javascript way of doing things. It turns out that asking about inheritance in Javascript is the wrong question and you shouldn't want that, at least not exactly.

Prototype
Every object in Javascript has a prototype property. This prototype object is specified at the Function level. Whenever you try to look up a property on an object (and remember, object methods are really just properties), if the object doesn't have the specified property, it will try to look it up on the prototype property. Let's say we have a class defined as such:
function Car() { 
} 
Car.prototype.wheels = 4; 
Car.prototype.drive = function() { /* do something */ }
And code that looks like:
var car = new Car(); 
var wheelCt = car.wheels;
As you would expect, wheelCt = 4. Here's how it got its value:
  1. look at car.wheels - the car object has no property called "wheels"
  2. get the property car.prototype which is the Car.prototype object
  3. look at the wheels property of this object, which is 4.
So now I want to create a Sedan object. I would like it to "inherit" the properties from Car. The way to do that is to instantiate a Car object and use this object as the Sedan prototype.
  1 Sedan.prototype = new Car() 
  2 Sedan.prototype.constructor = Sedan 
  3 function Sedan(color) { 
  4   this.color = color; 
  5 } 
  6 Sedan.prototype.doors = 4; 
  7  
  8 var newCar = new Sedan();
  9 var newWheelCt = newCar.wheels;
As you would expect, newWheelCt = 4.  Here's how it got its value:
  1. look at newCar.wheels - the sedan object has no property called "wheels"
  2. get the property newCar.prototype (call it sedanProto) which is the Car object we created on line 1
  3. look at the wheels property of sedanProto - it has no property called "wheels"
  4. Get the property sedanProto.protototype which is the Car.prototype object
  5. look at the wheels property of this object, which is 4.
We can, of course create further types and assign their prototypes to Sedan to increase this hierarchy as much as we want.

Oh, in case your curious about the line 2 - each prototype object has a constructor property which is the Function that it is associated with.  The can be used to determine what "type" an object is at runtime.  If we don't set it then Sedan's prototype's constructor will still be associated with Car which is not what we want.

Difference From Class Based Inheritance
Behaviorally this is pretty similar to inheritance as we all know it - Sedan gets the properties of Car. So how does it differ? Well, a big difference is that in Class based inheritance there is effectively one Car object for every Sedan object. With the prototype approach there is a single Car object, the one assigned to the prototype, and it is shared by all the Sedans. This means that you can't send parameters from the constructor of Sedan to the constructor of Car (see below for ways around this). You have to know the parameters for the superclass constructor at class declaration time, like below.
  1 function Polygon(sides) { 
  2   this.sides = sides; 
  3 } 
  4  
  5 Square.prototype = new Polygon(4);
  6 Square.prototype.constructor = Square 
  7 function Square(length) { 
  8   this.length = length; 
  9 }
So what if we wanted to add perimeter function to Polygon so all polygons have it. Something like:
Polygon.prototype.perimeter = function() {return this.length * this.sides;}
How does polygon get the length field? Obviously we could make the perimeter method take a length parameter, but that wouldn't make for a very good example. One thing that we could do, which is very disconcerting as a Java programmer, is nothing - it'll actually work as is.
var s1 = new Square(5);
var p1 = s1.perimeter();  // returns 20
Even though Polygon doesn't have a length property, because of JavaScript's dynamicness, at runtime everything is hunky-dory since Square's this does have a length property.

What if we really want Polygon to have a length property and not make every derived class specify this property? Maybe we have a base class with a bunch of instance properties that we want inherited? Well, there are a couple of approaches. One is to create a method that acts like a constructor in the base class and have the derived constructors call it.
Polygon.prototype.init = function(length) { this.length = length; }
function Square(length) { 
  this.init(length); 
} 
Another option is to write the base constructor so that it will handle 0 or all the arguments, and then call the base constructor:
  1 function Polygon(sides, length) { 
  2   this.sides = sides; 
  3   this.length = length; 
  4 } 
  5  
  6 Square.prototype = new Polygon();  // the prototype object has undefined sides and length property
  7 Square.prototype.constructor = Square 
  8 function Square(length) { 
  9   Polygon.call(this, 4, length);  // let Polygon's constructor update our properties
 10 }
Note that a new Polygon is NOT created on line 9.  Polygon's constructor is being executed on Square's this property.  One thing you have to watch out with this approach is that when we instantiate a Polygon to assign it to a Square prototype (line 6), we are not passing any arguments. As long as in the Polygon constructor we are just assigning them to properties, that is fine. However, if we are going to dereference them (e.g. to do more advanced calculations), we must first check that they are defined or else we will get the equivalent of a null pointer exception.

Wrong Approach
One solution that might seem like a good idea is to set the prototype in the constructor, as such:
  1 function Square(length) { 
  2   this.__proto__ = new Polygon(4, length); // bad idea
  3 } 
  4 Square.prototype.area = function() { return this.length * this.length; }
  5  
  6 var square = new Square(10);
  7 var sideCt = square.sides; // returns 4
  8 var area = square.area() // ERROR! no such method
This does give you access to the "superclass" properties, so line 7 will work as you would expect.  However, you are overwriting the default prototype object, so you lose all of its properties.  In the example above this means that the square object doesn't actually have the area method defined on line 4, which will cause an error to happen on line 8.  You'll also notice on line 3 that the actual name of the prototype property is __proto__, which is a good indicator that you shouldn't use it directly.

Summary Is-A vs. Has-A

In Class based inheritance, a derived class has an Is-A relationship with its parent class. An instance of the derived class is an instance of the parent class, just with more properties.  In the JavaScript prototype approach, the "derived" class Has-An instance of the "parent" class.  When property lookups are made on the derived instance, if it doesn't have the property it delegates the call to the parent object.  However, this automatic delegation can make the object behave as if it "is-a" parent object.  Which means I am going to stop writing now before I confuse you (or myself) any more.

No comments: