Simple Javascript Inheritance

There are lots of articles out there on the web explaining how to mimic the type of inheritance many of us programmers who come from Java or C# are accustomed to. Dean Edwards, Kevin Lindsay, and Douglas Crockford all come to mind. Each has done some very impressive work in with regards to Javascript inheritance.

Each of their approaches has its own advantages and disadvantages, and each approach is (at least in my mind) a little bit confusing. Heck, Douglass Crockford even talks about kinds of inheritance I've never even heard of!

I don't hesitate for a second to admit that each of these guys is probably a more skilled Javascripter than I am, but to be perfectly honest, well... I didn't completely understand their approaches. That doesn't mean they aren't good. On the contrary, it probably only serves to illustrate how much I still have to learn.

Regardless... I'm not completely comfortable using code I don't understand, so I took a crack at my own solution -- something really simple that I did understand. Perhaps my approach may not be as robust, but it's pretty darn simple to implement (and to use), and so far I've found it to be quite effective. So enough babbling... on to the technique.

I had just a few goals that I wanted my inheritance model to accomplish:

  1. Ability to extend any custom object using something like customObject.extends(baseObject);
  2. Ability to invoke the base object's constructor
  3. Ability to invoke an arbitrary base object's function
  4. Ability to override a base object's function

That's pretty much all I was after. I'm not tying to mimic multiple inheritance or anything like that -- just wanted to keep things short and simple. Here's what I came up with.

Function.prototype.extends = function(_baseObject)
{
   // create an instance of the base object
   var baseInst = new _baseObject();

   // copy all properties/functions defined on the base object with .prototype
   var key;
   for (key in _baseObject.prototype)
   {
      this.prototype[key] = _baseObject.prototype[key];
   }

   // expose the base object's constructor
   this.prototype.base = _baseObject;

   // expose an .invoke(..) to make it easy to invoke the base object's functions
   this.prototype.base.invoke = function(fn)
   {
      baseInst[fn].apply(this, Array.prototype.slice.apply(arguments, [1]));
   };
}

Explanation:

We begin by extending the prototype of the native Function object so that all custom classes you defined will automatically get the function, .extends(..).

Extending Function (and not Object) is important. If we added to Object's prototype, then every object would have a .extends property (even objects created with new Object() or {}). This can be problematic, as developers should be able to expect that a new, empty Object has no properties (this is important, for instance, when iterating over properties using for/in).

Next, we create a new instance of the base object, and stuff it into the variable baseInst. This baseInst will be how we enable the programmer to manually invoke a function of the base object (similarly to the way Java uses super.someMethod(..), albeit with a different syntax in our case).

Following the creation of the baseInst, we loop through the prototype of _baseObj and for each property of the base object's prototype, we create a corresponding property (with the same name/key) for the inheriting object. I could almost say this step is optional, but I won't (more on that later).

Next, we add a .base property to each object that is extended via our .extends(..) function which points to _baseObj. This will serve as a convenient way to invoke the base constructor. Once the .base property is created, we go one step further and expose a .invoke(..) function which will enable programmers to invoke any arbitrary function of the base object.

The .invoke(..) function isn't strictly necessary. All it really does is wrap up the native .apply(..) function, but to me it reads much cleaner when you're actually working with the code of your inherited object.

One final thing to note here is the use of Array.prototype.slice.apply(arguments, [1]). Credit totally goes to Douglas Crockford for this one. I knew that the arguments array was not a true array, but it never occurred to me to use .apply(..) again to get around that problem. In the past, I had always created a new array, args, and copied the values I needed from arguments into it. Douglas' approach though, is way more clever.

Usage:

Using this inheritance model really couldn't be easier. Here's how it works:

/* Shape object */
function Shape(_color)
{
   var color = _color;
   this.getColor = function() { return color; };
   this.draw = function() { alert('drawing shape...'); };
   this.calcArea = function() { alert('But I don\'t know what kind of shape I am yet!'); };
   this.talk = function(msg) { alert(msg); };
}
Shape.prototype.specialFn = function() { alert('I\'m a special function!'); };

/* Square object */
function Square(_color)
{
   this.base(_color);                     // invoke the base constructor
   this.draw = function()                 // override the .draw function
   {
      this.base.invoke('draw');           // invoke the base object's .draw function
      alert('drawing square...');
   };

   this.calcArea = function()             // override the base object's .calcArea function
   {
      alert('Length X Width');
   };
}
Square.extends(Shape);                    // Square extends the Shape object

/* Circle object */
function Circle(_color)
{
   this.base(_color);                     // invoke the base constructor
   this.draw = function()                 // override the .draw function()
   {
      this.base.invoke('talk', 'in the draw function');
      alert('drawing circle...');
   }
}
Circle.extends(Shape);

var s = new Square('blue');
s.draw();                                 // alerts "drawing shape..." followed by "drawing square..."
s.calcArea();                             // alerts "Length X Width"
alert(s.getColor());                      // alerts "blue"

var c = new Circle('green');
c.draw();                                 // alerts "in the draw function" followed by "drawing circle..."
c.calcArea();                             // alerrts "But I don't know what shape I am yet!"
alert(c.getColor());                      // alerts "green"
c.specialFn();                            // alerts "I'm a special function"

Most of the above sample should be pretty simple to follow. Obviously the most important part of using our inheritance code is the part that invoke the .extends(..) function. In the preceding example, we see it twice:

  • Square.extends(Shape);
  • Circle.extends(Shape);

Of course, that's only half of the equation. After indicating that both Square and Circle should extend the Shape object, we also need to make sure that we invoke the base constructor whenever we create a new instance of either a Square or a Circle. We do this first thing inside the functions that declare said objects.

function Square(_color)
{
   this.base(_color)              // invoke base constructor
   ...
}

function Circle(_color)
{
   this.base(_color)              // invoke base constructor
   ...
}

At this point, Square and Circle have both successfully "extended" the Shape object's prototype, meaning that they have all of the same public properties and functions as the Shape object.

At this point, we can go about overriding any functions of Shape that need their own specific logic, which would be functions like .draw(..) and .calcArea(..). Of course, in my sample, I didn't get into that much detail -- just some alert statements to illustrate the point, but I trust you get the idea.

Now, there are times when you may want to not only want to override a function, but also invoke the original logic of a base function. This can be achieved by calling this.base.invoke(..). The .invoke(..) function takes at least one parameter, and optionally, N more parameters.

The first parameter is always the name of the base function you want to execute (as a string, in quotes). Any additional parameters (and there can be an arbitrary number) will be passed into the base function when invoked.

I included a simple example of this in my sample code:

this.base.invoke('talk', 'in the draw function');

This code tells the inheriting object to call the base object's "talk" function and pass in one parameter, a string, "in the draw function".

Pretty straight forward right? At least I think so.

Final Thoughts:

Before wrapping this up, I want to touch on something I mentioned earlier and said I'd come back to:

for (var key in _baseObject.prototype)
   { this.prototype[key] = _baseObject.prototype[key]; }

The above code copies all of the properties (including functions) from the base object into the inheriting object. I said before that this almost wasn't necessary. Well it is necessary, specifically for those properties that are declared on the base object using .prototype.someFunction =.

Personally, I rarely ever do this when I write ECMAScript. I almost always keep my function encapsulated inside the object that "owns" them; that's just my style. But I recognize that the .prototype style is a common and viable approach, so we need to support it, and that's what the above for loop will do for us.

So there ya go...

This is my first attempt at writing any kind of encapsulated, reusable inheritance model in ECMAScript. I'm not necessarily claiming it's the best, or that it doesn't have its draw-backs, but it is pretty simple, and it does accomplish pretty much everything that I wanted. Hopefully some of you will find it useful as well.

Comments welcome.

2 Responses

  1. Daniel Austin Says:

    Hi,

    Your approach is basically the same as Josh Gertzen's:
    http://truecode.blogspot.com/2006/08/object-oriented-super-class-method.html. Works for me!

    Regards,

    D-

  2. Stephen Stchur Says:

    Thanks Daniel,

    I'm not familiar with Josh's work, but it's nice to know my approach is similar to at least one other person's — makes me feel somewhat validated.

Got something to say?

Please note: Constructive criticism is welcome. Rude or vulgar comments however, are not and will be removed during moderation.