If your previous experience with object-oriented programming has been in “classical” OOP languages like Java or C++, JavaScript’s prototype object system probably seemed strange at first. When I picked up JavaScript, it was one of the key parts for me to learn. I just wanted to see what possibilities it affords you and how a traditional class model with virtual methods, constructors, hidden state and inheritance could be mapped to JavaScript.
It turns out that a single class (if you can call it that in a classless language) is easy enough:
function Point(x, y)
{
this.x = x;
this.y = y;
}
Point.prototype.toString = function()
{
return "(" + this.x + "," + this.y + ")";
}
var p = new Point(1,2);
console.log(p.toString());
Running this code gives the expected result: p = (1,2).
Now let’s write a class Rect which extends Point and adds two more fields, for the width and the height of the rectangle. The standard approach you’ll find in JavaScript tutorials is to call the Point function to create a prototype for Rect:
1
2
3
4
5
6
7
8
9
10
11
| function Rect(x, y, w, h)
{
Point.call(this, x, y);
this.w = w;
this.h = h;
}
Rect.prototype = new Point();
var r = new Rect(3,4,5,6);
console.log(r.toString()); |
Since we haven’t provided a toString method for Rect, the call to toString is delegated to Point’s original implementation, so the result is (3,4).
Line 8 highlights the problem with this strategy: You have to call the superclass constructor to create the prototype! In many cases this is not an issue. In fact, in the Rect example above, the constructor will simply assign the value undefined to the fields x and y of the Rect prototype, but if you want to be able to treat the superclass as a black box, you need a way of subclassing it without calling its constructor on a dummy prototype object.
Unfortunately, you must call the constructor to create the prototype, but you can move the initialization code to a different function instead. The actual constructor can then be created automatically by this helper function:
function extcls(parent, init)
{
var cons = function cons()
{
if(arguments.length == 1 && arguments[0] === cons)
return;
init.apply(this, arguments);
};
cons.toString = function()
{
return "[Generated constructor for class "+init.name+"]";
};
cons.__init = init;
if(parent)
cons.prototype = parent.__init ?
new parent(parent) : new parent();
if(init.name) self[init.name] = cons;
return cons;
}
function defcls(init)
{
return extcls(null, init);
}
The Point and Rect classes can now be defined like this using defcls and extcls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| defcls(function Point(x, y)
{
this.x = x;
this.y = y;
})
Point.prototype.toString = function()
{
return "(" + this.x + "," + this.y + ")";
}
extcls(Point, function Rect(x, y, w, h)
{
Point.call(this, x, y);
this.w = w;
this.h = h;
}) |
Another problem remains with this code. On line 18, the Rect constructor has to call its super constructor Point() explicitly by name, and the syntax is similar when you override a method and want to call a method of the superclass. What’s missing here is Java’s super keyword. But I’ll leave that for another post.