Calling the "base" with prototypal inheritance in JavaScript
I wrote a little bit about how inheritance was designed to work in JavaScript sometime back. Briefly, inheritance in JavaScript is "prototypal" in nature and instead of types inheriting from other types, we have object instances inheriting from other object instances. Being a weakly typed language, the traditional interpretation of inheritance as it applies to a static strongly typed language does not apply here. Each object in JavaScript has a hidden reference to another object called its "prototype". Member references (i.e. when you access a property like so: obj.prop
) are resolved by the runtime by first looking the object itself up for the member and if not found then looking for it in the object's prototype. If the member is still not found then the prototype's prototype is inspected and so forth till the prototype of the root Object
object is encountered at which point the member is declared as non-existent. The blog post I linked to earlier talks about this in detail.
I am working on a project where I have an inheritance hierarchy such as the following:
var Base = Object.create({}, {
init: {
value: function() {
//
// default initialization here
//
print("Base.init");
}
}
});
var Derived = Object.create(Base, {
//
// override init
//
init: {
value: function() {
//
// call base here
//
print("Derived.init");
Object.getPrototypeOf(this).init.call(this);
}
}
});
Derived.init();
This produces the following output:
Derived.init
Base.init
This makes sense because I am calling init
on the Derived
instance and calling Object.getPrototypeOf
on Derived
should return a reference to Base
which we are subsequently able to use to invoke its (i.e. Base
's) version of init
. Wanting to make the syntax a tad more natural I decided to extend Object.prototype
to include an "uber
" getter property that will return a reference to the "base" object from which the current object inherits. Here's what I came up with:
Object.defineProperty(Object.prototype, "uber", {
get: function () {
return Object.getPrototypeOf(this);
},
enumerable: false,
configurable: false
});
This would now allow me to call the base like so:
var Derived = Object.create(Base, {
//
// override init
//
init: {
value: function() {
//
// call base here
//
print("Derived.init");
this.uber.init.call(this);
}
}
});
Derived.init();
Great. Now consider what happens in the following snippet:
var o1 = Object.create(Derived);
o1.init();
For all intents and purposes this perhaps looks like it should work (or it did so to me at least!). Turns out, this doesn't quite work the way we might expect it to. Let's step through what the JavaScript engine actually does when it encounters the call to init
via o1
.
- Does
o1
have an "own" (seeObject.hasOwnProperty
) property calledinit
? Nope, it does not. - Does
o1``s prototype have an "own" property called
init?
o1`s prototype is
Derivedand it does indeed have a property called
init. Great, invoke
Derived.initusing
o1as the object context (
Derived.init.call(o1)`). - The problem arises when the JavaScript engine encounters line 2 of
Derived.init
. Remember that "this
" here is a reference too1
and callingObject.getPrototypeOf
on it is simply going to result in a reference toDerived
being returned! So in effect, we are making a recursive call toDerived.init
!
Taking it to its logical conclusion it is easy to see that this code will result in Derived.init
being called over and over again till the JS engine runs out of stack space.
Adding a teeny bit of type metadata
If you think about the problem a bit you learn that the fundamental issue has to do with being able to somehow map member functions with the specific object where it is defined in such a way that this mapping is programmatically accessible at runtime. For example, we need to be able to associate the object Derived
with its implementation of the method init
. And this association must somehow be made accessible from Object.uber
so that it can return the correct prototype object reference. One rather straightforward way that we could perhaps achieve this is by simply passing Derived
to Object.uber
. Like so:
Object.prototype.uber = function (obj) {
return Object.getPrototypeOf(obj);
}
var Base = Object.create({}, {
init: {
value: function () {
//
// default initialization here
//
print("Base.init");
}
}
});
var Derived = Object.create(Base, {
//
// override init
//
init: {
value: function () {
//
// call base here
//
print("Derived.init");
this.uber(Derived).init.call(this);
}
}
});
var o1 = Object.create(Derived);
o1.init()
But I really wanted to be able to just say this.uber
instead of having to pass the reference to the object where the method in question has been defined. This however will be possible only if we extend Object
to include some additional support routines. First we add a function called inherit
that is defined like so:
Object.inherit = function (proto, props) {
var o = Object.create(proto, props);
Object.getOwnPropertyNames(o).forEach(function (pn) {
if (typeof (o[pn]) === "function")
o[pn]["___type___"] = o;
});
return o;
}
Object.inherit
simply delegates to Object.create
and performs one small activity in addition. It enumerates all the member methods in the newly created object and tacks on a property called ___type___
to them. This property will simply point back to the newly created object. This bit of metadata will then be used from uber
to retrieve a reference to the object where the method in question was created. With this new bit of metadata handy we modify uber
to look like this:
Object.defineProperty(Object.prototype, "uber", {
get: function () {
var o = arguments.callee.caller["___type___"];
if (!o) {
throw "Uh oh! No metadata!";
}
return Object.getPrototypeOf(o);
},
enumerable: false,
configurable: false
});
arguments.callee
refers to the current function, i.e., the function where the current line is executing. This is particularly useful in situations such as the above where the function in question is an anonymous function and there really isn't any other way to refer to it. Function objects have a magic property called caller
that become available as they are executing. Here's a small sample that illustrates this:
function bar() {
print(bar.caller);
}
function foo() {
bar();
}
foo();
print(bar.caller === null);
Here's the output we get on running this snippet:
function foo() { bar(); }
true
As you can tell, outside of the call context bar.caller
evaluates to null
but while it is executing, it contains a reference to foo
since bar
was invoked from foo
. We use this aspect of JS to access the function that invokes uber
which in turn should contain a reference to the object where the function was defined via ___type___
. Once we get that object it's a simple matter of returning its prototype. With all this support infrastructure in place, code like the following should work just fine:
var Base = Object.inherit({}, {
init: {
value: function () {
//
// default initialization here
//
print("Base.init");
}
}
});
var Derived = Object.inherit(Base, {
//
// override init
//
init: {
value: function () {
//
// call base here
//
print("Derived.init");
this.uber.init.call(this);
}
}
});
var MoreDerived = Object.inherit(Derived, {
init: {
value: function () {
print("MoreDerived.init");
this.uber.init.call(this);
}
}
});
//
// create instances that inherit from MoreDerived
//
var o1 = Object.create(MoreDerived);
var o2 = Object.create(o1);
o2.init();
Here's the output this produces:
MoreDerived.init
Derived.init
Base.init
OK, but what happens if somebody tacks on a new member method or overrides an existing one after Object.inherit
? Will this.uber
work from that method? Clearly, it won't because the hidden ___type___
member is added to the member functions only from Object.``inherit
. This means that we're going to have to add another little utility to Object
that wraps this little functionality. Here goes:
Object.override = function (o, name, fn) {
Object.defineProperty(o, name, {
value: fn
});
fn["___type___"] = o;
}
Now, we can add new member methods to objects using this function. Here's an example:
var o1 = Object.create(MoreDerived);
Object.override(o1, "init", function () {
print("o1.init");
this.uber.init.call(this);
});
var o2 = Object.create(o1);
Object.override(o2, "init", function () {
print("o2.init");
this.uber.init.call(this);
});
o2.init();
And here's the output we get:
o2.init
o1.init
MoreDerived.init
Derived.init
Base.init
That's it. As long as we use Object.inherit
and Object.override
to create our derived instances and to override methods respectively, we can confidently use this.uber
to refer to the correct parent prototype object at all times!