Severin Perez

Understanding Links on the JavaScript Object Prototype Chain

July 18, 2018

One of the things software developers think about a lot is how to DRY (Don’t Repeat Yourself) up their code. The reason for this is relatively simple. The more you can reuse your code, the easier your codebase is to maintain. If you have two objects that share behavior, why define that behavior in two places? And if you do, what happens if you need to change something later? All of the sudden you have to go searching through your codebase for every place that a given behavior is defined. So, DRY code is a good thing—but how do you do it? A common answer is to use classes and inheritance… but wait a minute, does JavaScript even have classes and inheritance? Well, no, it doesn’t, but it has something just as good—prototypal delegation.

Classical Inheritance

You may actually see the terms “class” and “inheritance” used in relation to JavaScript, but it’s important to understand that such terms are merely used for convenience’s sake. JavaScript does not have classes in the sense of a true class-based language (like Ruby for example), nor does it have classical inheritance. Don’t let the ES6 class keyword confuse you, it’s actually just syntactic sugar built on top of the common constructor object creation pattern in JavaScript (that is, a function called with the new keyword which returns an object). In a true class-based language you might have something that looks like the following:

class House {
  constructor(owner, rooms) {
    self.owner = owner
    self.rooms = rooms
  }
  
  ringDoorbell() {
    log "ding dong!"
  }
  
  describe() {
    log self.owner + "'s house has " + self.rooms + " rooms."
  }
}
bobHouse = new House("Bob Belcher", 4)
bobHouse.describe()       // "Bob Belcher's house has 4 rooms."

The above is not real code, but rather, a rough approximation of how a lot of class-based languages are written. In this example, we have defined a class House, which has a constructor and two methods. On instantiation, the constructor assigns two attributes to new objects of the House class. Each new House object also gets access to instance methods ringDoorbell and describe. If we create ten houses, all of them will be able to take advantage of these methods.

Class-based languages are powerful because you can use inheritance to create child-classes that share some behavior with their parent class, but can also define their own behavior. In cases where a particular child class needs to overwrite behavior defined on their parent class, they can do so using what is known as polymorphism. This is when a particular method might be re-defined on child classes with some new behavior. In our example, we might do something like define a subclass of House like this: class Castle < House { … } . Inside our new Castle subclass we might then re-define certain House behaviors or even add new ones.

So, we know what classes are, and how they use inheritance, but what does that mean for our JavaScript code if JavaScript doesn’t have real classes? Well, understanding classes and classical inheritance is going to help us define our mental model for JavaScript behavior. We have an idea now of what kind of behavior we want to produce (sharing like behavior between like objects), now we just need to see how to do it in JavaScript. The answer is something known as prototypal delegation.

Prototypal Delegation

In JavaScript, every object has what is known as a prototype object. Think of this relationship like links in a chain. When an object needs access to some behavior, it isn’t limited to just those methods formally defined as one of its own properties—it can also ask objects further up the chain for help. Consider the following:

var obj = {};

console.log(obj.toString());
  // Logs: [object Object]

Here we have an empty object, called obj, with no properties defined on it. And yet, when we call obj.toString() we get an output of [object Object]. How did that happen? Like every other object, our empty object obj has a link to a prototype—in this case, the default Object.prototype object. This prototype object has a built-in method defined on it called toString, and when we attempted to call that method on obj, obj looked for a property by that name on its own definition, discovered there was none, and then went up the prototype chain to keep looking.

Let’s take a look at this mysterious prototype object by using the built-in method Object.getPrototypeOf:

var obj = {};

console.log(Object.getPrototypeOf(obj));

/* Logs:
{ 
  constructor: ƒ Object(),
  hasOwnProperty: ƒ hasOwnProperty(),
  isPrototypeOf: ƒ isPrototypeOf(),
  propertyIsEnumerable: ƒ propertyIsEnumerable(),
  toLocaleString: ƒ toLocaleString(),
  toString: ƒ toString(),
  valueOf:  ƒ valueOf(),
  __defineGetter__: ƒ __defineGetter__(),
  __defineSetter__: ƒ __defineSetter__(),
  __lookupGetter__: ƒ __lookupGetter__(),
  __lookupSetter__: ƒ __lookupSetter__(),
  get __proto__: ƒ __proto__(),
  set __proto__: ƒ __proto__(),
}
*/

Now we are getting some insight into where obj went to go find the toString method. Object.getPrototypeOf(obj) returns obj’s prototype and we can see that it does indeed have a toString method defined on it. This is prototypal delegation in action. Our simple obj object delegated the toString behavior to its prototype. Note how this is different from inheritance. obj does not have its own toString method, rather, it relied on its prototype to define that behavior. Of course, object’s don’t have to rely on other objects up their prototype chain, nor are they forever stuck with the implementation of some behavior up that chain. If we wanted to, we could redefine the toString behavior directly on obj and get a new result:

var obj = {};

obj.toString = function() {
  console.log("I'm an object!");
};

obj.toString();
  // Logs: "I'm an object!"

In this example, we defined a new toString behavior on obj and when we subsequently called toString on it, obj found a method by that name among its own properties and thus had no need to delegate the behavior up its prototype chain.

Linking Objects

Now that we know how prototypal delegation works, let’s explore how we can use it to our advantage. First, we’re going to define an object with a set of behaviors that we expect we are going to need on a regular basis. We will store this object in a variable that has a capitalized name (this isn’t required, but it’s a useful convention). Next, we will create a new object and use Object.create to link our new object back to our first object. By doing this, we are manually building the prototype chain so that we can be sure that lower-level objects are able to delegate behavior up to higher-level objects. Let’s look at an example:

var House = {
  ringDoorbell: function() {
    console.log("ding dong!");
  },
  
  describe: function() {
    console.log(this.owner + "'s house has " + this.rooms + " rooms.");
  }
};

var bobsHouse = Object.create(House);

console.log(Object.getPrototypeOf(bobsHouse));
  // Logs: {ringDoorbell: ƒ, describe: ƒ}

console.log(Object.getPrototypeOf(bobsHouse) === House);
  // Logs: true

bobsHouse.describe();
  // Logs: "undefined's house has undefined rooms."

bobsHouse.owner = "Bob Belcher";
bobsHouse.rooms = 4;

bobsHouse.describe();
  // Logs: "Bob Belcher's house has 4 rooms."

Here, we have a House object that defines ringDoorbell and describe methods. We then create a variable, bobsHouse and set its prototype as House. We can see this relationship using Object.getPrototypeOf, which clearly shows that House is the first-level prototype for bobsHouse. Now, bobsHouse can delegate behavior up its chain to methods defined on House. We do this on line 21 when we use the call the describe method on bobsHouse. Of course, the output isn’t quite right because describe relies on its calling object having access to attributes called owner and rooms. Once we set these attributes on bobsHouse we see that describe functions as intended.

All of this can get a bit confusing. In this example, there are only two objects in bobsHouse’s prototype chain (House at the first level and Object.prototype at the level above that.) But imagine a longer prototype chain—how would you know whether an object was using some behavior defined among its own properties or delegating that behavior up the chain? Thankfully, we can check for exactly that using the built-in methods hasOwnProperty (you may have noticed this one earlier when we saw it defined on the Object.prototype object) and Object.getOwnPropertyNames. Consider the following:

var House = {
  ringDoorbell: function() {
    console.log("ding dong!");
  },
  
  describe: function() {
    console.log(this.owner + "'s house has " + this.rooms + " rooms.");
  }
};

var bobsHouse = Object.create(House);
bobsHouse.owner = "Bob Belcher";
bobsHouse.rooms = 4;

console.log(Object.getOwnPropertyNames(bobsHouse));
  // Logs: ["owner", "rooms"]

console.log(bobsHouse.hasOwnProperty("rooms"));
  // Logs: true

console.log(bobsHouse.hasOwnProperty("ringDoorbell"));
  // Logs: false

console.log(Object.getPrototypeOf(bobsHouse).hasOwnProperty("ringDoorbell"));
  // Logs: true

In this example, we use Object.getOwnPropertyNames on line 17 to return an array of the properties that are defined directly on bobsHouse. We can see that one of them is rooms, which we subsequently confirm on line 20 when we check whether bobsHouse has its own property called rooms. On line 23 we see that bobsHouse does not have its own property called ringDoorbell, but it still has access to such a behavior by virtue of its prototype, House, which on line 26 we see does have its own property by that name.

Prototypal delegation is a powerful way to define a behavior in one place and then use it in many others. If we wanted, we could create a thousand houses, using House for the prototype of each, and they would all have access to the behaviors defined on House. And what if later we decided that houses should have garages and be able to open them? No problem. We could simply add a new method to House, called openGarage, and every individual house that was linked to the House object would have access to that behavior.

Multiple Links in the Chain

Earlier we said that one of the advantages of classical inheritance was the ability to create subclasses. In JavaScript, we don’t create subclasses but we can still leverage prototypal delegation to achieve similar results. We do this by creating multiple links in the prototype chain. Let’s try it out.

var House = {
  ringDoorbell: function() {
    console.log("ding dong!");
  },
  
  describe: function() {
    console.log(this.owner + "'s house has " + this.rooms + " rooms.");
  }
};

var Castle = Object.create(House);
Castle.describe = function() {
  console.log(this.owner + " owns a mighty castle with " + this.rooms + " rooms!");
};

var hearstCastle = Object.create(Castle);
hearstCastle.owner = "William Randolph Hearst";
hearstCastle.rooms = 56;

hearstCastle.describe();
  // Logs: "William Randolph Hearst owns a mighty castle with 56 rooms!"

hearstCastle.ringDoorbell();
  // Logs: "ding dong!"

Like before, we have a House object, but now we want to create things on, shall we say, a grander scale. Now, we want to make castles too. First, we define a Castle object and set House as its prototype. But castles and houses aren’t exactly alike so we probably shouldn’t describe them the same way. To fix this, we simply define a new version of the describe behavior on the Castle object. Finally, we create an individual castle called hearstCastle and set Castle as its prototype. As you can see, when we call describe on hearstCastle it uses the behavior by that name defined on Castle rather than the one defined on House. This is because as JavaScript searches up the prototype chain for a behavior it stops as soon as it finds one with the right name. Of course, inserting links between two objects doesn’t prevent the lower-most objects from using behaviors on higher-level objects, which is why hearstCastle still has access to ringDoorbell, which was defined way back on House.

To illustrate the chain in action, let’s see if we can access each object along it and take a look at its properties. We can do this using a short recursive function that will crawl up a given object’s prototype chain and log the properties defined on each link. The recursive function ceases to call itself as soon as it encounters a null value, which is what you get when you try to look at the prototype of the top-level Object.prototype object.

function crawlPrototypeChain(obj) {
  console.log("-----------------");
  console.log("Object: ", obj);
  console.log("Own Properties: ", Object.getOwnPropertyNames(obj));
  
  var objPrototype = Object.getPrototypeOf(obj);
  if (objPrototype) {
    crawlPrototypeChain(objPrototype);
  }
}

var House = {
  ringDoorbell: function() {
    console.log("ding dong!");
  },
  
  describe: function() {
    console.log(this.owner + "'s house has " + this.rooms + " rooms.");
  }
};

var Castle = Object.create(House);
Castle.describe = function() {
  console.log(this.owner + " owns a castle with " + this.rooms + " rooms!");
};

var hearstCastle = Object.create(Castle);
hearstCastle.owner = "William Randolph Hearst";
hearstCastle.rooms = 56;

crawlPrototypeChain(hearstCastle);

/* LOGS: 
-----------------
Object:  {owner: "William Randolph Hearst", rooms: 56}    // hearstCastle
Own Properties:  ["owner", "rooms"]
-----------------
Object:  {describe: ƒ}                                    // Castle
Own Properties:  ["describe"]
-----------------
Object:  {ringDoorbell: ƒ, describe: ƒ}                   // House
Own Properties:  ["ringDoorbell", "describe"]
-----------------
Object:  {constructor: ƒ,                                 // Object.prototype
          __defineGetter__: ƒ,
          __defineSetter__: ƒ,
          hasOwnProperty: ƒ,
          __lookupGetter__: ƒ, 
          … }
Own Properties: ["constructor", "__defineGetter__", "__defineSetter__", 
                 "hasOwnProperty", "__lookupGetter__", "__lookupSetter__",
                 "isPrototypeOf", "propertyIsEnumerable", "toString", "valueOf",
                 "__proto__", "toLocaleString"]
*/

As the recursive function executes, starting with hearstCastle, it logs out the current object and its own properties and then gets that object’s prototype so it can continue up the chain. This gives us a higher-level view of the properties defined at each step and if/when they are re-defined at lower steps (for example, where both Castle and House have a property called describe.)

TL;DR

Classical inheritance, such as that used in a language like Ruby, provides a mechanism to define behavior on a class, create subclasses that inherit that behavior (or overwrite it). Once classes are defined, you can then instantiate objects from a class, which will be able to use the instance methods defined on their class. JavaScript does not have an inheritance system like this but the same functionality can be achieved using prototypal delegation. Each object in JavaScript has a prototype object and any object lower on the prototype chain can use the properties defined on objects up the chain. Or, lower-level objects can define their own behavior by the same name, which they will then use rather than trying to delegate the behavior up the chain. In order to check whether a given object has a property defined on itself, or if it is delegating that behavior up its prototype chain, you can use the Object.getOwnPropertyNames and Object.prototype.hasOwnProperty methods.


That’s all for our introduction to the object prototype chain. There is a lot more to discover here, particularly how prototypes are related to constructor functions. But that’s a story for another day. In the meantime, if you want to learn more about how to leverage prototypal delegation, check out this article on common object design patterns.


Note: This article was originally published on Medium.


You might enjoy...


© Severin Perez, 2021