Let's Make a Framework: Classes, Inheritance, Extend
Not all JavaScript frameworks provide classes. Douglas Crockford discusses the classical object model in Classical Inheritance in JavaScript. It’s an excellent discussion of ways to implement inheritance in JavaScript. Later, he wrote Prototypal Inheritance in JavaScript in which he basically concludes prototypal inheritance is a strong enough approach without the classical object model.
So why do JavaScript libraries provide tools for OO programming? The reasons vary depending on the author. Some people like to ape an object model from their favourite language. Prototype is heavily Ruby inspired, and provides Class which can be useful for organising your own code. In fact, Prototype uses Class internally.
In this article I’m going to explain prototypal inheritance and OO, and start to create a class for OO in JavaScript. This will be used by our framework, turing.js.
Objects and Classes vs. Prototype Classes
Objects are… everything, so some languages attempt to treat everything as an object. That means a number is an object, a string is an object, a class definition is an object, an instantiated class is an object. The distinction between classes an objects is interesting — these languages treat classes as objects, and use a more basic object model to implement classes. Remember: it’s object oriented programming not class oriented.
So does that mean JavaScript really needs classical classes? If you’re a Java or Ruby programmer you might be surprised to find JavaScript doesn’t have a class keyword. That’s OK though! We can build our own features if we need them.
Prototype Classes
Prototype classes look like this:
function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype.toString = function() {
return 'x: ' + this.x + ', y: ' + this.y;
}
v = new Vector(1, 2);
// x: 1, y: 2
If you’re not used to JavaScript’s object model, the first few lines might look strange. I’ve defined a function called Vector, then said new Vector(). The reason this works is that new creates a new object and then runs the function Vector, with this set to the new object.
The prototype property is where you define instance methods. This approach means that if you instantiate a vector, then add new methods to the prototype property, the old vectors will get the new methods. Isn’t that amazing?
Vector.prototype.add = function(vector) {
this.x += vector.x;
this.y += vector.y;
return this;
}
v.add(new Vector(5, 5));
// x: 6, y: 7
Prototypal Inheritance
There’s no formal way of implementing inheritance in JavaScript. If we wanted to make a Point class by inheriting from Vector, it could look like this:
function Point(x, y, colour) {
Vector.apply(this, arguments);
this.colour = colour;
}
Point.prototype = new Vector;
Point.prototype.constructor = Point;
p = new Point(1, 2, 'red');
p.colour;
// red
p.x;
// 1
By using apply, Point can call Vector‘s constructor. You might be wondering where prototype.constructor comes from. This is a property that allows you to specify the function that creates the object’s prototype.
When creating your own objects, you also get some methods for free that descend from Object. Examples of these include toString and hasOwnProperty:
p.hasOwnProperty('colour');
// true
Prototypal vs. Classical
There are multiple patterns for handling prototypal inheritance. For this reason it’s useful to abstract it, and offer extra features beyond what JavaScript has as standard. Defining an API for classes keeps code simpler and makes it easer for people to navigate your code.
The fact that JavaScript’s object model splits up portions of a class can be visually noisy. It might be attractive to wrap entire classes up in a definite start and end. Since this is a teaching framework, wrapping up classes in discrete and readable chunks might be beneficial.
A Class Model Implementation Design
The previous example in Prototype looks like this:
Vector = Class.create({
initialize: function(x, y) {
this.x = x;
this.y = y;
},
toString: function() {
return 'x: ' + this.x + ', y: ' + this.y;
}
});
Point = Class.create(Vector, {
initialize: function($super, x, y, colour) {
$super(x, y);
this.colour = colour;
}
});
Let’s create a simplified version of this that we can extend in the future. We’ll need the following:
- The ability to extend classes with new methods by copying them
- Class creation: use of
applyandprototype.constructorto run the constructors - The ability to determine if a parent class is being passed for inheritance
- Mixins
Extend
You’ll find extend littered through Prototype. All it does is copies methods from one prototype to another. This is a good way to really see how prototypes can be manipulated — it’s as simple as you think it is.
The essence of extend is this:
for (var property in source)
destination[property] = source[property];
Class Creation
A create method will be used to create new classes. It will need to handle some setup to make inheritance possible, much like the examples above.
// This would be defined in our "oo" namespace
create: function(methods) {
var klass = function() { this.initialize.apply(this, arguments); };
// Copy the passed in methods
extend(klass.prototype, methods);
// Set the constructor
klass.prototype.constructor = klass;
// If there's no initialize method, set an empty one
if (!klass.prototype.initialize)
klass.prototype.initialize = function(){};
return klass;
}
Get the Code
I’ve already created a basic OO class for turing in turing.oo.js. You can read it and experiment with it now.
I’ll continue this part next Thursday!