Douglas Crockford has given
another talk on the future of Javascript, with particular attention to what he now considers the Right Way to do object-oriented programming, which is quite a departure from classical thought. Years ago, he stopped using
new explicitly, and came up with
Object.create to use instead, for more straightforward use of Javascript's prototypal inheritance design.
Now, he has also stopped using
this, which means he has effectively abandoned Javascript's notion of inheritance altogether. He calls his new style "class-free object-oriented programming", the basic feature of which is that every object actually contains its own methods, rather than sharing them in a prototype or parent object. This is "inefficient" by some measure, but if you think about it, the sharing of methods is simply an optimization, and in most cases, optimizing for space in JS is not going to make a critical difference.
It happens that this was the style I adopted when I got into programming JS, because I was not trained up in classical object-oriented languages. Ironically, more recently, I did get into the typical use of
new and prototypes, but I'm going to reconsider that for a while.
Crockford's
basic boilerplate for a constructor is:
function constructor(spec) {
let {member} = spec,
{other} = other_constructor(spec),
method = function () {
// accesses member, other, method, spec
};
return Object.freeze({
method,
other,
});
}
Which is to say that there is
no built-in inheritance; objects that are composed-in are simply member sub-objects. You can, of course, make pass-through member calls.
Danny Fritz
blogged about the concept of class-free OO programming, with some helpful illustrative examples, but his techniques differ somewhat from Crockford's, notably in the use of
this. Predictably, I have a take that is somewhat different from each of them, but which I think marries their best features.
Crockford uses constructors for everything, which has the code smell of boilerplate in the form of the
return Object.freeze portion. Fritz has constructors and extenders, with a copy-in
extend function. I have always hated such extenders, and I don't see a reason to have a distinction between constructors and extenders.
Instead, I propose that the proper separation of duties is to have a universal constructor and everything else is an initializer. This gives you freedom to inherit and allows you to avoid the security hazards of
this.
The universal constructor looks like this:
Function.prototype.construct = function (spec) {
var obj = {};
this(obj, spec);
return Object.freeze(obj);
}
Almost too simple to bother with, but it takes care of the boilerplate issue. Note: I formerly had defined this as
Object.construct, but this definition makes every function be a potential constructor, which seemed more appropriate.
Now, for any object type (or extension type), all you have to concern yourself with is the initialization. I will illustrate using Fritz's examples of alligator, duck, and goat objects that are defined in terms of extensions Animal, Walking, Swimming, Flying. This is a straightforward port of his alligator:
function alligator (self, spec) {
self.name = 'alligator';
self.word = 'grrr';
animal(self);
walking(self);
swimming(self);
}
All of the extensions are just initializers called on the
self object (which, being passed in, avoids the security problems of
this), and the alligator function is, itself, just an initializer.
spec is not used in this case, but I have kept it to show how Crockford's model fits. Most objects do have inputs to their initialization. Similarly,
animal et. al. would normally take a second argument.
To create an instance:
var myAlligator = alligator
.construct(spec);
It should be clear from this how duck and goat would similarly be created, but let's take a look at the extensions. Again, a straightforward port of animal:
function animal(self) {
self.name = 'name';
self.word = 'word';
self.talk = function () {
console.log(self.name + ' says ' + self.word);
}
};
It becomes clear at this point that all the things that inherit from animal have a name and a word. Initialization of those should be done with a spec, rather than having placeholder defaults and each derived object having to know about them:
function animal(self, spec) {
self.name = spec.name;
self.word = spec.word;
self.talk = function () {
console.log(self.name + ' says ' + self.word);
}
};
Now we can rewrite alligator:
function alligator (self, spec) {
animal(self, spec);
walking(self);
swimming(self);
}
alligator.construct({name: 'alligator', word: 'grrr'});
So our alligator doesn't merely contain animal, walking, and swimming objects (as would be the case in the Crockford model), but it has been initialized to have those traits directly. This requires that all the traits play nicely with each other. If that can't be guaranteed, Crockford's model offers more security, and can obviously be implemented with pass-through methods like so:
function alligator (self, spec) {
animal(self, spec);
var walkTrait = walking.construct(); var swimTrait = swimming.construct(); self.walk = walkTrait.walk;
self.swim = swimTrait.swim;
}
alligator.construct({name: 'alligator', word: 'grrr'});
Note that I have initialized the alligator
self as an animal, then added the other features as traits. Whether you initialize on the object itself or create sub-objects is up to you.