JavaScript under the hood: Prototype Chain
In my previous article I talked at length about constructor functions,which are a way of creating objects in JavaScript. In order to invoke a constructor function, we add the new keyword before a function call, like so:
const person = new Person();
Putting the new keyword in front of any function changes that function from a function call to a construction call. It does 4 things (besides executing the function)
1.Creates a brand new empty object.
2.This empty object gets linked to it’s prototype object.
3.The brand new object gets bound to the this object.
4.If the function doesn’t return anything, the new object is returned.
This article is a deep-dive into the second step; the prototype object, which is JavaScript’s way of sharing properties and methods across objects. Mastering this concept is essential for developers looking to write elegant, efficient and maintainable code. This article is based on the excellent PluralSight course called “Advanced JavaScript” by Kyle Simpson.
Javascript has an inbuilt function called Object. This function has a property called prototype, which is an object containing properties and methods provided by the JavaScript environment; one such property on the prototype object is called constructor, which points to Object. In other words, the function Object has a reference to an object called prototype, which in turn has a reference back to the Object function via it’s constructor property. If that sounds confusing, I’ve drawn a diagram that should help. The blue circle represents a function, and the green rectangle represents an object.
You’ll note that one of the properties on the prototype object is ** proto ** which is actually a getter function that returns the prototype object of whatever the this binding is. If this is confusing, don’t worry, we’ll talk about this property in great detail later. As a side note, you’ll probably agree that it’s quite cumbersome to pronounce ** proto ** (i.e.underscore underscore proto underscore underscore), so the verbiage coined by the JS community for this prop is dunder proto.
Let’s learn more about the prototype mechanism by analyzing the code below:
function Person(name) {
//line 1
this.name = name; //line 2
}
Person.prototype.greet = function () {
// line 3
return "Hello, " + this.name; //line 4
};
const p1 = new Person("John"); // line 5
const p2 = new Person("Jane"); // line 6
p2.greetLouder = function () {
//line 7
return "HELLLOOO " + this.name; // line 8
};
console.log(p1.constructor === Person); //line 9 (outputs true)
console.log(p1.prototype); // line 10 (outputs undefined)
console.log(p1.__proto__ === Person.prototype); // line 11 (outputs true)
console.log(p1.__proto__ === p2.__proto__); //line 12 (outputs true)
We have already learnt how lines 5 and 6 are executed:
1.A new object is created.
2. The object gets linked to Person’s prototype object.
3. The object gets set to this.
4. The object is returned.
Lines 9-12 they seem pretty innocuous at first, but if you console.log(p1) you’d see that there is neither a constructor nor ** proto ** property on p1, and perhaps even more surprisingly, greet isn’t a property on p1 either. So what’s going on ?
When we try to access a property/method on an object, the runtime searches the scopes of that object to see if said property/method exist. If it doesn’t, it traverses up the prototype chain of the object and sees if the property exists on the object’s prototype, and if it’s not found it will search that object’s prototype and so on until the property is found or we’ve hit the end of the prototype chain. Keeping this in mind, when line 9 is executed the runtime basically says “hey, constructor isn’t a property on p1, let me look at it’s prototype object and see if the property exists there. Sure enough, constructor is a property on Person.prototype (it’s one of the many inbuilt properties provided by default by the JavaScript environment). This process of an object inheriting properties/methods from other objects that are linked to it via the prototype chain is known as prototypal inheritance.
It’s important to note that the prototype property is only available on the constructor functions, NOT on the instances themselves, which is why when we execute line 10:console.log(p1.prototype), the output is undefined. The instances get a reference to their constructor function’s prototype object via an internal property called [[Prototype]]. You can’t access this property directly, which is why it’s grayed out when you console.log(p1) above but when the runtime traverses up an object’s prototype chain, it is via this property that it’s traversing. So when line 11 is executed, the runtime will not find ** proto ** on p1, so it will traverse up it’s protoype chain (via the [[Prototype]] prop) to Person.prototype. As you’ll soon learn, ** proto ** is also NOT a property on Person.Prototype, so then the runtime will go up Person.prototype’s chain to Object.Prototype. Turns out there’s a property ** proto ** on Object.Prototype, which is basically a getter function which returns the prototype of whatever the this binding is (I mentioned this earlier!). Well, the this binding of p1 is Person, so the runtime will return Person’s prototype object.
If the above seems a little confusing, that’s because it is! To solidify the concept, I’ve drawn a diagram which shows the prototype chain linkage from p1 all the way up to Object.prototype. As a reminder, objects are green squares and functions are blue circles.
There are two major benefits of delegating function calls up the prototype chain:
- We save a tremendous amount of memory by not copying over functions and properties unless we really need to. Imagine that we constructed a thousand variations of Person, and that there were a hundred methods that were defined inside of the constructor function like so:
function Person(name) {
this.name = name;
this.greet = function () {
return "Hello ," + this.name;
};
//100 more methods
}
var p1 = new Person("John");
/// 1000 more Person objects
The JavaScript thread will need to allocate memory for a hundred thousand functions as opposed to only a hundred functions when prototypal inheritance is used.
- We can consolidate logic in one place, making the code more maintainable. Folks who’re coming to JavaScript from an object oriented language are especially partial to overriding/extending methods that are defined in the parent class. Let me explain using code:
public class Dog {
//mark the method as 'virtual' so the compiler knows it can be overwritten
public virtual void Greet() {
Console.WriteLine("Woof woof!");
}
}
//ExcitedDog extends Dog
public class EnergeticDog : Dog {
public override Greet() {
WagTail(); //add additional functionality before calling the base method;
base.Greet();
}
}
As a side note, I personally think there isn’t much benefit to be gained from pretending JavaScript is an object oriented(OO) language or shoe-horning OO paradigms in JS; I’m hoping that through understanding the prototypal inheritance we’ve gained a deep appreciation for the beauty and elegance of JS’s underlying mechanism.
We’ve looked at how powerful a mechanism prototypical inheritance can be; but it’s not without flaws. A major drawback of prototypical inheritance is that the ** proto ** property is NOT readonly, i.e. it can be changed.
For example, look at the following code:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return "Hello ," + this.name;
};
var p1 = new Person("John");
p1.__proto__ = null; //the __proto__ is NOT a readonly prop and can be changed at will
p1.greet(); //Uncaught TypeError: p1.greet is not a function
Here we see that all the benefits of prototypical inheritance can be destroyed in one fell swoop at the hands of a careless(or malicious) developer. Indeed, one of the reasons that the ** proto ** property has double underscores preceding and succeeding it and is not simply called something like superProto is to reduce the probability of it being overwritten.
Prototypal inheritance is one of the most powerful and commonly used patterns in JavaScript. I hope that this article was helpful in demystifying the more confusing aspects that underlie it. In future articles we’ll explore design patterns that employ prototypal inheritance, so stay tuned!