Yoan Blanc’s weblog

Another lost swiss guy

JavaScript prototypal chaining for computer scientists

Yoan BlancSat, 11 Apr 2009, , ,

Prototypal chaining (aka inheritance) is something that toke me a couple of years to really understand, simply because you usually don’t use it directly (because a library/framework does it for you) or you don’t need it. I failed a job interview question on that one year ago. And when I recently explained prototypal chaining to a friend of mine, he suggested that I should write something about it. To make it very simple and easy.

I got introduced to Object-oriented programming (OOP) using the C++ and Java languages, with the very infamous Animal class. JavaScript is a prototypal language, so is a little bit different than Java, C++ or C#. For the next 5 minutes, try to forget what you already know and dive into what follows.

Let’s start by altering the prototype of Object. (Code tested running SpiderMonkey or inside the Firebug console.)

>>> Object.prototype.foo = "bar";
>>> var a = {}; // or new Object()
>>> a.foo
"bar"
>>> // with Firebug: console.log
>>> for(var key in a) {
...  print(key);
... }
bar
>>> "foo" in a
true
>>> a.hasOwnProperty("foo")
false

object.prototype example

a is an Object that as prototype. The prototype of Object don’t have any prototypes, Object is the root element. But the key foo is a String that has a prototype that is an Object. Yes there is a loop.

(Almost) everything in JavaScript is an object and every Object as a prototype.

>>> a.foo.foo.foo.foo.foo
"bar"
>>> new Date().foo
"bar"
>>> 3.14.foo
"bar"
>>> (42).foo
"bar"
>>> true.foo
"bar"
>>> (function(){}).foo
"bar"
>>> "spam".foo
"bar"
>>> /^[rR](?:e[gx]){2}p$/.foo
"bar"
>>> // almost everything
>>> undefined.foo
TypeError: undefined has no properties
>>> TypeError.foo
"bar"

I hope this shows that you should never alter Object.prototype.

Another (evil) example with different native types.

Date.prototype example

>>> Object.prototype.foo = "bar";
>>> var now = new Date();
>>> now.foo
"bar"
>>> Date.prototype.foo = "spam";
>>> now.foo
"spam"
>>> new Object().foo
"bar"
>>> delete Date.prototype.foo
>>> now.foo
"bar"

This shows how the language walks from prototype to prototype looking for a key. It’s basically a chain. How to get the prototype of any thing in JavaScript? Usually any object has a constructor attribute, but try to use duck-typing instead.

>>> new Date().constructor == Date
true
>>> .1.constructor == Date
false
>>> .1.constructor == Number
true

Gecko (the engine powering Firefox) offers a very handy property on any element __proto__ that returns the prototype. I’ll not use it here to avoid any confusions. It’s the only way I know to get the prototype of a prototype.

Let’s forget this quickly and start doing something interesting, something that looks like inheritance but relies on prototypal chaining.

Shape and circle prototype example

>>> function Shape(name) { this.name = name };
>>> Shape.prototype.toString = function() {
...  return "<Shape \""+this.name+"\">"
... };
>>> new Shape("blob");
<Shape "blob">
>>> function Circle(radius) {
...  Shape.call(this, "circle");
...  this.radius = radius
... }
>>> Circle.prototype = new Shape();
>>> Circle.prototype.constructor = Circle;
>>> Circle.prototype.area = function() {
...  return this.radius * this.radius * Math.PI
... };
>>> var circle = new Circle(1);
>>> circle
<Shape "circle">
>>> circle.area()
3.14159

The tricky thing here is those two lines:

>>> Circle.prototype = new Shape();
>>> Circle.prototype.constructor = Circle;

The first one says that the prototype of a Circle is a Shape. It’s an instance of a Shape. And the constructor is set back to enable testing instances as you’ve seen previously. As the graph shows, an instance of a Circle has two elements name and radius, the function area belongs to its prototype. This prototype is a Shape so it has a name too, its value is undefined here.

This is one way of doing prototypal inheritance, another way would have be by doing a copy of the methods of one object’s prototype to the other one. And because everything is an object, this is not a real copy, just another reference pointing to the same values.

In the previous example, calling the function toString will have to walk up two levels of prototypes in order to find it. Let’s us an alternative way.

>>> for(var key in Shape.prototype) {
...  if(Shape.prototype.hasOwnProperty(key) &&
...     key !== "constructor")
...   Circle.prototype[key] = Shape.prototype[key]
... }

By doing so, let’s look at the result graph.

Shape and circle prototype example

The graph is more flat that before, it keeps everything at the same level, which is good because walking up prototypes might take time but you cannot monkeypatch a whole chain of prototype by touching only one of them. It’s a tradeoff.

To conclude, let’s compare two ways of doing (apparently) the same thing.

>>> function Foo() {
...  this.toString = function() {
...   return "Foo";
...  }
... }
>>> new Foo();
"Foo"
>>> function Bar() {}
>>> Bar.prototype.toString = function() {
...  return "Bar"
... };
>>> new Bar();
"Bar"

Calling new Foo().toString() will be very direct, the function belongs to the instance. So doing new Bar().toString() will have to look into the prototype to find the function to call. Foo is faster but will take more memory because toString will be duplicated in every instances and Bar will be a little slower but creating many instances won’t duplicate toString because it lives into the prototype and not the instance. And you can modify (monkeypatch) the toString method of a Bar for all the instances at once.

I’ve made a little test that creates a huge chain of prototypes and count how many time is spent walking up (or down) it. It takes some time but only when the chain is insanely long. So don’t (premature) optimize on that. Run the test.

L’article en version intégrale et francophone a été publié sur dosimple.ch, pour permettre aux illustrations et explication de mieux respirer. Car j’ai pour une fois fais deux versions identiques. « Quelle barbe ! » me direz-vous. Que voulez-vous, les temps sont durs.

Merci de votre fidélité et bonne lecture.

About

meYoan Blanc is a web developer that lives in Switzerland (Jaquet-Droz 6, 2300 La Chaux-de-Fonds) and works as a freelance.

Get my vCard or contact me by phone (skype:yoan.blanc) or email ().

Misc

RSS, list.blogug.ch

This site reflects only my opinion and is not affiliated with anyone else.

copyright 2006-2009 — doSimple.ch