DEV Community

Zach Taylor
Zach Taylor

Posted on • Edited on

Inheritance in JavaScript

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

let product = new Product(10)
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Product.prototype.sayPrice = function() {
  return `I cost $${this.price}`
}
Enter fullscreen mode Exit fullscreen mode

We could now call sayPrice on the product instance.

product.sayPrice()
// Output: I cost $10
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Now if we create a book with

let book = new Book(154, 10)
Enter fullscreen mode Exit fullscreen mode

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

book.sayPrice()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Book.prototype.__proto__ === Product.prototype
// Output: true
Enter fullscreen mode Exit fullscreen mode

We can now run

let book = new Book(154, 10)
book.sayPrice()
// Output: I cost 10
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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

Book.prototype = Object.assign(Book.prototype, Product.prototype)
Enter fullscreen mode Exit fullscreen mode

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}`
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)