I’ve been writing heavy-duty JavaScript for about six years. I’ve used lots of libraries that provide “classes” of various types—ExtJS, Dojo, JavaScriptMVC, and more. Yet none of them felt right. In retrospect, I feel like the standard class-based inheritance is not good fit for the language. Perhaps that’s because classical inheritance can be shoe-horned into JavaScript, butprototypal inheritance is how the language is built.
The other day, I ran across someone else with the same attitude. In fact, while I was thinking about it, he was talking and writing code to make it real. He is Eric Elliot and he wrote Stampit.
Stampit
Stampit is an interesting and small library designed to make it easy to use prototypal inheritance. In a game that I ported from Python to JavaScript, I used prototypal inheritance in the traditional way:
// Not exactly the code you’ll find in the game, but close enough for
// illustration.
//
FunnyBoat.Boat = function(application, game) {
// ...
}
FunnyBoat.Boat.prototype = {
ahead: function(x) { /* ... */ },
// ...
}
I had several diffent “actor” classes (think actors on a stage, not the actor pattern) that had different other roles. One of the player’s character—a boat. Another was a pirate ship. Still another was a jumping shark. These have some things in common: floating, sinking, and dealing and taking damage. They have some differences: jumping, shooting, and controling inputs.
It became clear that it would be useful to build the actors by composition. I eventually hacked together some solutions that worked, but were not elegant. When I saw Stampit, I knew something better had come.
Stampit has a handy compose method that applies the other Stampit methods to work composition magic. First, I’ll start with a naive state machine for jumping actors:
/*jshint asi:true*/
var stampit = require('stampit')
var jumpingActor = stampit().enclose(function() {
var kJumpDuration = 2 * 1000 // ms
var kStateDown = 'down'
var kStateUp = 'up'
var jumpTimer = null
var state = kStateDown
function OnFinishJump() {
// assert kStateUp == state
jumpTimer = null
state = kStateDown
}
this.Jump = function() {
if (kStateDown == state) {
jumpTimer = setTimeout(OnFinishJump, kJumpDuration)
state = kStateUp
}
}
this.GetJumpingState = function() {
return state
}
})
There is no rocket science in this code. There is a shared private state variable, two public methods, and a private method. Next, I’ll add a similarly naive state machine for shooting:
var shootingActor = stampit().enclose(function() {
var kReloadDuration = 3 * 1000 // ms
var kStateReadyToShoot = 'ready'
var kStateReloading = 'reloading'
var reloadingTimer = null
var state = kStateReadyToShoot
function OnFinishReload() {
// assert kStateReloading == state
reloadTimer = null
state = kStateReadyToShoot
}
this.Shoot = function() {
if (kStateReadyToShoot == state) {
reloadTimer = setTimeout(OnFinishReload, kReloadDuration)
state = kStateReloading
}
}
this.GetShootingState = function() {
return state
}
})
Note that I’m using the state variable name again. If you were in a traditional class, you would have to use separate state names (or better yet, separate state machine objects.)
Here’s a simple function for logging the states:
var stateReporting = stampit.convertConstructor(function() {
return stampit.extend(this, {
ReportStates: function() {
console.log(this.GetJumpingState(), this.GetShootingState())
}
})
})
Using the Stampit compose method, we create a factory that puts all these together in a single object, and test the code:
var composedActorFactory = stampit.compose(
jumpingActor,
shootingActor,
stateReporting)
global.actor = composedActorFactory()
actor.ReportStates()
actor.Shoot()
actor.ReportStates()
actor.Jump()
actor.ReportStates()
setTimeout(function() {actor.ReportStates()}, 2500)
setTimeout(function() {actor.ReportStates()}, 3500)
When I run it, I see this:
$ node demo.js
down ready
down reloading
up reloading
down reloading
down ready
Yay for encapsulation and composition in a pretty format! For a slightly deeper walk-through and more information on why should consider prototypal inheritance (with or without Stampit), see Eric Elliot’s page on Three Kinds of Prototypal OO.