Nerdworks logo "The nerd shall inherit the earth."

Nerdworks Blogorama

Nerdspeak

Calling the "base" with prototypal inheritance in JavaScript
Technobabble
7/11/2011 11:09:34 AM

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.

  1. Does o1 have an “own” (see Object.hasOwnProperty) property called init? Nope, it does not.
  2. Does o1‘s prototype have an “own” property called init? o1‘s prototype is Derived and it does indeed have a property called init. Great, invoke Derived.init using o1 as the object context (Derived.init.call(o1)).
  3. The problem arises when the JavaScript engine encounters line 2 of Derived.init. Remember that "this" here is a reference to o1 and calling Object.getPrototypeOf on it is simply going to result in a reference to Derived being returned! So in effect, we are making a recursive call to Derived.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!

 
Sankarsan Bose 7/14/2011 9:03:57 PM
Excellent post!!!
 
Ranju V 7/14/2011 9:54:13 PM
Thanks Sankarsan!
 
kpozin 4/10/2012 1:50:42 PM
Unfortunately, "caller" and "callee" are deprecated (and are forbidden in strict mode). Not sure if there's any workaround.
 

Please fill this form and click on the "Submit" button to post a comment. All fields except the comment box are optional. You don't have to give me your name and email, but if you do then that might allow me to follow up with you on your comment. Also, I won't publish your email address here or anywhere else.

 
Your Name :
Your Email :
Your Comment :
   

What in your opinion do you get when you multiply the number 5 by the number 2?

Your answer will help me figure out whether you are human or a spam bot. If you're a spam bot I hope your kernel core dumps and your CPU bursts into flames.

   

Please click here to go back to the blog.

blogorama home
about this blog
email the author
where on earth am i?
subscribe to mailing list
feeds Use these links for feed syndication
rss  |  atom
by category
technobabble (58)
philosophical crud (3)
irrelevant stuff (7)
archive
november, 2011 (2)
october, 2011 (1)
september, 2011 (7)
july, 2011 (3)
june, 2011 (2)
may, 2011 (3)
april, 2011 (1)
march, 2011 (1)
february, 2011 (1)
february, 2010 (1)
october, 2009 (1)
september, 2009 (1)
july, 2009 (5)
march, 2009 (2)
august, 2008 (2)
march, 2008 (1)
january, 2008 (1)
september, 2007 (2)
april, 2007 (1)
february, 2007 (2)
december, 2006 (1)
october, 2006 (1)
september, 2006 (4)
august, 2006 (3)
july, 2006 (4)
june, 2006 (3)
may, 2006 (6)
april, 2006 (2)
recent entries
Screen scraping wit...
Building an Instagr...
Building an Instagr...
Organizing your Jav...
289583 hits