This blog post explains JavaScript’s prototypal inheritance in a simple way. As it turns out, if we initially leave out constructors, then it is easy to understand. Thus, we first look at the fictional programming language ProtoScript which is JavaScript minus constructors, explain it, and then move on to constructors. As a result, you should have a solid understanding of prototypal inheritance and won’t be confused, any more, by all the JavaScript tricks out there.
When looking for data, the search starts at this and traverses a chain of prototype objects, where each object is a prototype for its predecessor. If something cannot be found in an object, the search continues in the prototype. |
In class-based languages, whenever we have a set of similar objects, say persons with a name that can be converted to a text string, we create a class for that set. In ProtoScript, which does not have classes, we create an initial person, e.g.
var person0 = { name: null, describe: function() { return "Person "+this.name; } };All other persons are copies of person0. The only drawback of this approach is that the method describe() exists in each instance. We can avoid this kind of duplication by putting the method into the prototype. person0.describe() will execute the method in the prototype. Inside it, the expression this.name will find the correct property in person0, because this is still bound to person0. Naturally, for the above mentioned sharing to occur, copying person0 must not copy the prototype, the copy must be shallow.
The object person1 is a (modified) copy of person0. The object ProtoPerson is their prototype and shared by both. |
var ProtoWorker = { __proto__: ProtoPerson, describe: function() { return this.title+" "+this.name; } }; var worker0 = { __proto__: ProtoWorker, name: null, title: null };
The prototype of worker0 extends the prototype of person0. As invoked via worker0, the method describe() of ProtoWorker overrides the same method of ProtoPerson. |
To summarize: Like JavaScript, ProtoScript does not have classes, only objects. But the special property __proto__ and a clever way of sharing some objects, while copying others allow us to do everything that class-based languages can. Like in other prototype-based languages, we have the pleasure of always directly manipulating objects and there are fewer abstractions involved such as classes or instantiation. This makes prototype-based languages more intuitive for beginners. The next section explores JavaScript’s constructors.
function Person(name) { this.name = name; } Person.prototype.describe = function() { return "Person "+this.name; }; var person = new Person("John");The function Person is a constructor for persons. We need to invoke it via new Person(). The operator new creates an object with a property __proto__ that points to Person.prototype and hands it to Person() as the implicit parameter this. We have thus arrived at the same result as with ProtoScript, but getting there was a bit more weird. The constructor Person combines the ProtoScript operations of defining the initial object and copying it. Note that the property __proto__ is non-standard, some browsers do not support it. But we need it to implement workers as an extension of persons, in the code below.
function Worker(name, title) { Person.call(this, name); this.title = title; } Worker.prototype.__proto__ = Person.prototype; Worker.prototype.describe = function() { return this.title+" "+this.name; };We invoke Person without new to set up the inherited properties (which we weren’t able to do in ProtoScript). If you want to invoke a method that you have changed, you invoke it via the prototype of the “super-constructor” as follows. Only the code of the method “changes”, the value of this stays the same. This is because earlier properties in a prototype chain can hide later ones and we have to avoid un-hiding them.
Constr.prototype.mymethod = function(x, y, z) { SuperConstr.prototype.mymethod.apply(this, arguments); }apply() is similar to call(). Its first argument is also the value for this. But where call() has one additional argument for each parameter of the method, apply() only has a single second argument with a list of values for the parameters. Thus, you can use the array-valued variable arguments.
var ProtoPerson = { describe: function() { return "Person "+this.name; } }; function createPerson(name) { // factory function return Object.create(ProtoPerson, { name: { value: name } }); } var person = createPerson("John"); // no "new"If you wanted to make sure that createPerson() is never invoked via new, you could check that this is undefined. If you need to extend persons, like we did with workers, the property definitions inside createPerson() have to become an externally accessible entity. A hypothetical createWorker() would then use property definitions which have been (non-destructively!) derived from that entity.