Abstract. This posting shows how one can make Crockford's power constructor functions play nicely with the JavaScript keyword 'instanceof.'
[
update: March 18, 2008. I asked Crockford what he thinks about this pattern, and he actually discourages the use of 'instanceof' -- instead, he prefers to "... rely instead on good design and polymorphism."]
The inheritance model of JavaScript is based on a combination of the 'new' keyword and the prototype property of (constructor) functions. JavaScript Guru Douglas Crockford (aka 'Yoda') argues that this model (which he calls pseudo-classical) is awkward. Instead, he proposes an elegant, powerful and simple model (parasitic inheritance), using so-called power constructor functions. Note, familiarity with the above concepts is necessary for complete understanding of this post [and something that every web developer should know anyway ;-)].
The advantages of power constructor functions include support for private, shared and public variables as well as simplicty (avoiding new and prototype). There is a mismatch, however, between constructors and the JavaScript keyword,
instanceof. Consider the following example:
var object = (function() {
function F() {}
return function(o) {
F.prototype = o;
return new F();
};
})();
var OriginalPerson = {
sayHello: function(){
return "Hello, my name is "+this.getName();
},
getName: function(){return 'Adam';}
};
function Person(name) {
var p = object(OriginalPerson);
p.getName = function() {return name;};
return p;
}
function Guru(name,topic) {
var g = object(Person(name));
g.getTopic = function() {return topic;};
return g;
}
var karl = Person('Karl');
var crockford = Guru('Douglas','JavaScript');
karl instanceof Person;
crockford instanceof Guru;
Hmm... Clearly, any environment that makes
crockford instanceof Guru evaluate to
false must be getting something wrong!
In general, one has to do
something to make super-constructors work with
instanceof. The expression
exp1 instanceof exp2 where
exp1,exp2 are JS expressions (
exp2 must evaluate to a function and
exp1 should evaluate to an object) works with following semantics:
First
exp1 is evaluated, say, to
o then
exp2 is evaluated, say, to
f. If
o is an object and
f is a function, the entire expression evaluates to true only if following
o's [[prototype]] chain, we can reach
f.prototype.
This means that to make this work, we must ensure that the object created has
Person.prototype or
Guru.prototype in its prototype chain.
What I would really like to end up with at the end of this blog entry, is to be able to write code similar to:
var OriginalPerson = {
sayHello: function(){
return "Hello, my name is "+this.getName();
},
getName: function(){return 'Adam';}
};
var Person = OriginalPerson.parasite(function(Host,name) {
var p = object(Host());
p.getName = function() {return name;};
return p;
});
var Guru = Person.parasite(function(Host, name,topic) {
var g = object(Host(name));
g.getTopic = function() {return topic;};
return g;
});
Guru('Douglas Crockford','JavaScript') instanceof Guru;
Guru('Douglas Crockford','JavaScript') instanceof Person;
The extra parameter
Host is supposed to represent the "Host" of the parasite (i.e., Person in the case of Guru), the idea being that the
parasite function will somehow 'wrap' the
Person function to set it up so that 'instanceof' works, and then finally feed this wrapped function to the parasite (via the Host variable). I won't be able to write the code exactly as above, but we will get close... Anyway, hopefully this will make more sense very soon!
We will get there in two steps. First we code it up manually (so to speak) and secondly we will do the meta-programming with
parasite.
Incidentally, we can exploit Crockford's 'shared secrets' technique to get the prototype chain working. Consider the following code.
function Person(name, proto) {
var p = object(proto || Person.prototype);
p.getName = function() {return name;};
return p;
}
Person.prototype = {
sayHello: function(){
return "Hello, my name is "+this.getName();
},
getName: function(){return 'Adam';}
};
Person('Karl') instanceof Person;
The key here is the statement:
object(proto || Person.prototype). This ensures that the object created has
Person.prototype in its [[prototype]] chain. The
proto ||-part is intended to be used as a 'shared secret' between a parasite 'sub type'/'sub power constructor'; the invariant is that
proto || Person.prototype will always denote an object which is
Person.prototype or has it in its prototype chain. It can be used as follows:
function Guru(name,topic,proto) {
var g = object(Person(name, proto || Guru.prototype));
g.getTopic = function() {return topic;};
return g;
}
Guru.prototype = object(Person.prototype);
Guru('Douglas Crockford','JavaScript') instanceof Guru;
Guru('Douglas Crockford','JavaScript') instanceof Person;
Actually, I feel some pleasure in the assignments:
Person.prototype = {
sayHello: function(){
return "Hello, my name is "+this.getName();
},
getName: function(){return 'Adam';}
};
and
Guru.prototype = object(Person.prototype);: Intutively, these objects are the 'prototypes' of the objects created by the power constructors, so it feels natural to make this assignment.
So far so good - we have instanceof working with power-constructors, but we can do better. The problem now is to do the meta-programming that will handle the 'secret-sharing' behind the scenes.
In this example, we will enhance
Object.prototype and
Function.prototype. For simplicity I've introduced the constraint that all power-constructor functions should take only one argument [however, I'm convinced this can be generalized to more than one argument].
Note, slightly off topic here...
In either case, I've developed at taste for single-argument functions. Consider:
@param
@return
function Guru(conf){
var g = object(Person(conf)),
t = conf.topic || 'none';
g.getTopic = function(){return t;};
return g;
}
Guru({name:'Yoda', topic:'JavaScript'});
The disadvantage is that there is slightly more code to write. The advantages are (1) readability:
Guru({name:'Yoda', topic:'JavaScript'}) can be read without having to consult the constructor function about which argument is the name and which is the topic; (2) optional/default arguments: you can leave out either of the arguments:
Guru({name:'Yoda'}) or
Guru({topic:'JavaScript'}) are both valid (in the multiple arg constructor with name as the first parameter, you'd have to write
Guru('Yoda') (which is fine), and
Guru(undefined,'JavaScript') (which is not).
Back on track
The following is my implementation. I extend
Object.prototype and
Function.prototype so that we can write:
var OriginalPerson = {
sayHello: function(){
return "Hello, my name is "+this.getName();
},
getName: function(){return 'Adam';}
};
var Person = OriginalPerson.parasite(function(Host, conf) {
var p = object(Host()),
name = conf.name || 'Anonymous';
p.getName = function() {return name;};
return p;
});
var Guru = Person.parasite(function(Host, conf) {
var g = object(Host(conf)),
topic = conf.topic || 'none';
g.getTopic = function() {return topic;}
return g;
});
var h = Guru({
name: 'Douglas Crockford',
topic: 'JavaScript'
});
h instanceof Guru;
h instanceof Person;
I've implemented it as follows (with comments):
Object.prototype.parasite = function(parasite){
function wrapper(conf,proto) {
var p = proto;
Array.prototype.splice.call(arguments,0,0,function(){
return object(p || wrapper.prototype);
});
return parasite.apply(this, arguments);
}
wrapper.prototype = this;
return wrapper;
};
Function.prototype.parasite = function(parasite) {
var host_cons = this;
function wrapper(conf,proto) {
var wrapper_this = this,
p = proto;
Array.prototype.splice.call(arguments,0,0, function() {
Array.prototype.splice.call(arguments,arguments.length,0,
p || wrapper.prototype);
return host_cons.apply(wrapper_this,arguments);
});
return parasite.apply(this, arguments);
}
wrapper.prototype = object(this.prototype);
return wrapper;
};