Prototype: Inheritance Madness

—Tuesday, May 23 2006

UPDATE: October 9th, 2007 – This article is no longer relevant for 1.6+. Please see: http://prototypejs.org/learn/class-inheritance

Prototype has a philosophy that you either love or hate. There was once a time where Prototype modified Object.prototype, causing jaws to drop and eyes to bulge. Some were so turned off by the decision they decided to create their own Javascript libraries.

All that has since changed. Thanks to the persuasion of The Javascript MacGyver, Sam replaced the Object.prototype bits in favor of Object.extend and Object.inspect. With this decision, however Prototype’s inheritance scheme can be messy, and it’s certainly hard to read if you don’t spend time trying to really figure out what’s going on.

Inheritance as it was

When Object.prototype.extend was changed to Object.extend it caused problems. Before the change it was easier to understand what was going on:

var Shape = Class.create();
Shape.prototype = {
  initialize: function(options) {
    this.options = options;
    this.width = 100;
    this.height = 100;
  },
  
  draw: function() {
    console.log('drawing');
  }
}

var Square = Shape.extend({
  initialize: function(options) {
    this.options = options;
  }
});

This is easy enough to understand, however it still has it’s problems. You overwrite any methods in the superclass when a method of the child class has the same name. We’ll go on about how we can fix this later, but for now, lets look at how you extend objects in Prototype in its current state.

Inheritance as it is

There are a couple different ways you can do inheritance in Prototype, each having its own unique behavior.

Extending the prototype, not initializing the parent

var Square = Class.create();
Square.prototype = Object.extend(Shape.prototype, {
  initialize: function(options) {
    this.options = options;
  }
});

In this example, we’ve extended the Shape.prototype object directly. If you load this up in Firebug’s console and execute new Square({sides: 4}), then inspect the object you’ll see that we do indeed get the draw method and our options have been set. For those paying close attention, you’ll notice that we did not inherit the superclass’s width and height properties.

Prototype-Inheritance-1

We didn’t inherit these properties because the initialize method of the superclass was never called, we have overwritten it with our subclass. So we can conclude that this method of inheritance is only ideal in unique situations where we don’t have a need to initialize a parent class. There are other hackish ways to work around this. You could name the initialize method of the superclass to something like baseInitialize and call it in your sublcass constructor. It’s ugly, but it works. This also assumes you never need to instantiate the parent class directly.

Extending An Anonymous Instance

One way to get around the parent class initialization problem is to extend an anonymous instance of it. Change your Square code to this:

var Square = Class.create();
Square.prototype = Object.extend(new Shape(), {
  initialize: function(options) {
    this.foo = 'I am foo';
    this.options = Object.extend({
      color: 'red'
    }, options || {});
  }
});

If you load this up in your console and create a new Square you’ll see that this works pretty well. We’ve successfully initialized the Shape object and our Square object, and we have everything we’d expect to see in our instance.

So what’s wrong with this approach? The anonymous object gets created instantly when the page is loaded. So lets see where this approach backfires:

var Shape = Class.create();
Shape.prototype = {
  initialize: function(options) {
    this.options = options;
    this.width = 100;
    this.height = 100;
    
    this.draw();
  },
  
  draw: function() {
    console.log('drawing');
  }
}


var Square = Class.create();
Square.prototype = Object.extend(new Shape(), {
  initialize: function(options) {
    this.foo = 'I am foo';
    this.options = Object.extend({
      color: 'red'
    }, options || {});
  },
  
  draw: function() {
    console.log(this.options);
  }
});

When you create a new instance of Square here, which draw method do you think will be called? Since we’ve overwritten draw in our subclass, you would expect it to come from Square right? Lets see what happens when we load this code up in the browser.

Prototype Inheritance

The first line “drawing” is shown instantly. What gives though? We’ve overwritten this method right? Well yes and no. Because we created a new instance of Shape when we loaded the page, it knows nothing about our draw method in our subclass. It’s just hanging around, waiting to be acquainted with our subclass when we actually instantiate it.

So what happens when we instantiate our Square class? Since we are initializing the parent, we should get the correct draw method called right? I’m afraid it doesn’t work that way. The last two lines of the image above show what we get. The Shape class doesn’t reinitialize when we create an instance of Square, we are merely extending an instance of Square that was created when the page loaded.

Fixing the Problem

Trying to describe the quirks of inheritance with Prototype in my book on Prototype is a challenge to say the least. I don’t think it was ever Sam’s intentions to have this type of quirky inheritance scheme, but when Object.prototype.extend was changed, so did the rules.

There has got to be a better way to handle this. It turns out that the guy who broke the original way came up with a better way. Dean Edwards has came up with a beautiful way of handling Javascript inheritance with Base (which he seems to be updating at the time of this article…hows that for timing Dean) ;-).

At the moment I’ve resorted to using Base.js with Prototype and so far it has worked flawlessly. I’ve written a complete replacement for plotKit using it (more on this later). It makes adding new chart types and extending the library super easy.

Prototype needs a replacement for the the Class and Object.extend bits, and I personally feel that we need to integrate something like Base into Prototype before it gets to be too late. We can retain backwards compatibility and also provide a new, and much better way to work with inheritance.

Sam, accepting patches for this? ;-)