Inheritance in JavaScript

September 29, 2020

As someone who learned to code in C++, JavaScript was confusing to me when I first started learning it. One of the main reasons for my confusion was that the object-oriented concepts I was used to were seemingly nowhere to be found. Sure, you could create objects in JavaScript, but to me, they seemed more like a map in C++ or a dict in Python.

I certainly didn't understand how inheritance could work, and for a while, I just assumed that inheritance wasn't possible in JavaScript. Turns out, inheritance is possible in JavaScript, but it is done through the use of prototype objects.

Prototype Objects

The Mozilla docs explain prototype objects like this:

When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain.

So, in JavaScript, there is no such thing as a class in the traditional sense. There are only objects. Each object has an attribute called a prototype that you can access via obj.__proto__. When you try to access an attribute on an object instance, JavaScript will first check the attributes of that object. If the attribute is not found there, it will check the object's prototype, then the prototype's prototype, and so on, until it finds the attribute or reaches the end of the prototype chain.

An Example

To define an object type in an object-oriented language like Java or Python, you would create a class and give it a special method called a constructor. For example, to define a Product object type in Java, you could write

public class Product {
  double price;

  public Product(double price) {
    this.price = price;
  }
}

To define an object type in JavaScript, you only create the constructor. For example, we could create a Product constructor like this.

function Product(price) {
  this.price = price;
}

To create a new object using the Product constructor, you would do

let product = new Product(10);

When you call new in this line, the variable product gets injected as this in the Product constructor. Therefore, if you log the output of product.price you should get 10.

Now what if we wanted to add a method to all instances of Product? In Java, we would add a method to the class, like this.

public class Product {
  double price;

  public Product(double price) {
    this.price = price;
  }

  public String sayPrice() {
    return "I cost $" + String.valueOf(this.price);
  }
}

In JavaScript, we add the method as an attribute on Product's prototype.

Product.prototype.sayPrice = function () {
  return `I cost $${this.price}`;
};

We could now call sayPrice on the product instance.

product.sayPrice();
// Output: I cost $10

If you log Product.prototype and product.__proto__ you will notice that they are the same object. When JavaScript sees that sayPrice is not an attribute of product, it checks its __proto__ and finds the sayPrice method there.

Inheritance

Now suppose we wanted to define a Book object type that inherits from Product. In Java, we would extend the base class.

public class Book extends Product {
  int pages;

  public Book(int pages, double price) {
    super(price);
    this.pages = pages;
  }
}

In JavaScript, we would create a Book constructor, inside of which we call the Product constructor, passing this as the context.

function Book(pages, price) {
  Product.call(this, price);
  this.pages = pages;
}

Now if we create a book with

let book = new Book(154, 10);

we will get an object that has both the price and pages attributes. However, if we try to call

book.sayPrice();

we will get a TypeError: book.sayPrice is not a function. This is because we haven't updated Book's prototype object. To fix this, we can do

Book.prototype = Object.create(Product.prototype);

This sets Book's prototype to an empty object whose prototype is Product.prototype. In other words,

Book.prototype.__proto__ === Product.prototype;
// Output: true

We can now run

let book = new Book(154, 10);
book.sayPrice();
// Output: I cost 10

and JavaScript will find sayPrice on book.__proto__.__proto__.

There is one other thing I should mention. By default, every constructor's prototype is an object with one attribute, constructor, which is a reference to the constructor itself. You can check this by running

Product.prototype.constructor === Product;
// Output: true

When we set Book.prototype = Object.create(Product.prototype), we overwrote Book's default prototype, so book.constructor === Product, which isn't correct. We can fix this by either adding the constructor property afterwards

Book.prototype = Object.create(Product.prototype);
Object.defineProperty(Book.prototype, "constructor", {
  value: Book,
  enumerable: false,
  writable: true,
});

or by replacing the Object.create line with Object.assign

Book.prototype = Object.assign(Book.prototype, Product.prototype);

Now Book.prototype.constructor === Book should be true.

What About JavaScript's class Keyword?

JavaScript does have a class keyword, but it's just syntactic sugar for what we did above. It allows you to create the constructor and set methods on the prototype all in one block. For example, we could create an equivalent Product type with

class Product {
  constructor(price) {
    this.price = price;
  }
  sayPrice() {
    return `I cost $${this.price}`;
  }
}

If you log Product.prototype, you'll see that it is an object with both constructor and sayPrice properties, just like we had before. We can also create the Book sub type with

class Book extends Product {
  constructor(pages, price) {
    super(price);
    this.pages = pages;
  }
}

Defining Book this way will automatically set Book.prototype and ensure that book.constructor is correct.

Conclusion

Objects in JavaScript are a bit confusing at first since they are quite different than in most other languages. I encourage you to open the dev tools in your browser and play around with these examples to get a feel for them. I hope this helps you understand JavaScript a little better!