Intro

So right after learning about classes and the benefits of using them, we just got hired at a new gig working for Williams Sonoma, a home goods store. It's holiday time right now so they're bringing in a bunch of new developers that can help them with their big holiday sale on all kinds of home products.

We're tasked to start managing their coffee maker data, but ultimately we're going to move onto all categories of products. So using what we know about classes, and their ability to model any kind of data, let's create a generic class called Product

class Product {}

And for every product, we're just going to have a few properties for now. They'll each have a name, a price, and whether they can be discounted as part of the sale. Not all of the products in the collection are eligible for a discount. So now let's make all of these instance properties in our constructor. Since we just touched on how to use constructors in classes, try to do this on your own...

When we instantiate our class, name, price and discountable will be passed through to our constructor, and using it, we'll make them all instance properties on this:

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this.price = price;
    this.discountable = discountable;
  }
}

And as we mentioned, our first product will be a coffee maker, let's start with the nespresso one from the home page:

const product1 = new Product("Nespresso Coffee Maker", 99.95, true);

So this is very good--we're making new objects with our Product class and we can use it to make any other product throughout the store.

Problems without inheritance

So this process continues and we've made a bunch of our products, however, some of our products are going to be on sale, that is discountable, and some aren't. And for the sale itself, we've now learned that we need to add some new properties and features to our created products, such as the percent that will be taken off the price when its on sale, for example, 10% off.

Couldn't we just add the additional properties that we need to the Product class and recreate our individual products? We certainly could but there are couple of problems:

  1. It would be a lot of work to recreate each of our products, depending on the amount we have to update (try to imagine if we have thousands of products)
  2. This is contrary to how we know prototypical inheritance works

We want the objects to all inherit the stuff that they need and now we're proposing to go back and change them manually. This is definitely a move in the wrong direction. So what can we do that's better?

Making use of other classes' functionality

Instead of changing classes after we've created them, why don't we just create a new class and take the stuff we need from the old one? We're aware of the shareable nature of prototypes, can't we just link one classes prototype with the other? In fact, we can.

In our case, for our products on sale, we can create a new class called SaleProduct. Notice how its name is a modified variant of Product, which is the class we're to going to take properties from, so this name implies the shared relationship between the two classes:

class SaleProduct {}

With this new class we can have it 'inherit' or borrow all of the properties of another one by using the extends keyword. SaleProduct extends (that is, uses the properties from) the Product class:

class SaleProduct extends Product {}

So for our new sale product, we want to use all of the properties of Product while adding some new ones. To do that, once again, we'll make a constructor to add those properties. Now to create a sale product, we want to add the additional information of the percentage that the product is going to be discounted by. So we'll call that value percentOff. So when we make a saleproduct, that value will be provided along with all of our previous product info.

class SaleProduct extends Product {
  constructor(percentOff) {
    this.percentOff = percentOff;
  }
}

Now let's create a SaleProduct instead of a Product. We'll include all of the values we did before. As we mentioned, the goal is to inherit all of the properties of Product's constructor, so we should just be making a normal product just with this additional percentOff property.

Let's say that coffee makers are all 20 percent off:

const saleProduct1 = new SaleProduct("Nespresso Coffee Maker", 99.95, true, 20);

// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

However, we're getting an uncaught referenceerror. Must call super constructor in derived class before accessing 'this'.

So what in the world does this mean? If we look at our constructor, we're using or 'accessing' the this keyword. But we can't do that yet. As the error says, we need to first call the super constructor in the derived class.

super

What does that mean?

The derived class is the class we are extending or inheriting stuff from and if we want to do so, we need to tell that class to use it's constructor. We do that with the function super().

super is a function used in the constructor that calls the constructor of the class we are extending.

Calling super is the equivalent of saying, hey other constructor, create an instance of your class for me with these values! In our case, name, price and discountable. And then our parent's instance, when its created, is provided on 'this' to add our own properties. So, in other words, if we are extending a class but we don't call super in our constructor, the parent instance won’t be created.

So to first create a Product instance, let's pass the product data passed as arguments to SaleProduct over to Product's constructor using super. We'll get access to them in the constructor and call super with them:

class SaleProduct extends Product {
  constructor(name, price, discountable, percentOff) {
    super(name, price, discountable);
    this.percentOff = percentOff;
  }
}

const saleProduct1 = new SaleProduct("Nespresso Coffee Maker", 99.95, true, 20);
console.log(saleProduct1); // SaleProduct {name: "Nespresso Coffee Maker", price: 99.95, discountable: true, percentOff: 20}

And note that finally we have the behavior that we wanted. Upon extending the Product class and using super, we are using Product's constructor to make us an instance of Product and adding to it the percentOff property, as well. Note that if we want, we can also overwrite properties that were created by the extended class, for example we could not pass discountable to super and instead create it how we want within SaleProduct, say making it the opposite of the value that was passed in:

class SaleProduct extends Product {
  constructor(name, price, discountable, percentOff) {
    super(name, price);
    this.discountable = !discountable;
    this.percentOff = percentOff;
  }
}

const saleProduct1 = new SaleProduct("Nespresso Coffee Maker", 99.95, true, 20);
// SaleProduct {name: "Nespresso Coffee Maker", price: 99.95, discountable: false, percentOff: 20}

Sharing methods

Now what about sharing methods from the extended or parent class as it is called, since we know how to share properties to make custom variants of existing classes?

Now with a sale product, we want to figure out its sale price, but we first need to see whether it is discountable first. If it is discountable, we'll display the sale price, otherwise we'll tell the user this product is not part of the sale.

Within the Product class, we can create a very basic method to tell us whether the product itself is eligible for a discount:

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this.price = price;
    this.discountable = discountable;
  }

  isDiscountable() {
    return this.discountable;
  }
}

And with that, let's use it in determining whether to calculate our sale price or not. So this will be done using a method in the SaleProduct class. We want to call the isDiscountable method from the parent class to write this function. To do that, we can simply use super. We don't need to call it though, we can use it like an object and the methods we want as a method off of it:

class SaleProduct extends Product {
  ...

  getSalePrice() {
    if (super.isDiscountable()) {
      return this.price * ((100 - this.percentOff) / 100);
    } else {
      return `${this.name} is not eligible for a discount`;
    }
  }
}

So now to get the sale price for our coffee maker:

const saleProduct1 = new SaleProduct("Nespresso Coffee Maker", 99.95, true, 20);
console.log(saleProduct1.getSalePrice()); // 79.96

And if at any time, we decide that the product is not discountable (say the company prevents us from discounting it), we see our other result:

const saleProduct1 = new SaleProduct(
  "Nespresso Coffee Maker",
  99.95,
  false,
  20
);
console.log(saleProduct1.getSalePrice()); // 'Nespresso Coffee Maker is not eligible for a discount'

Problems with inheritance

So at this point, classes seem like a great way to add properties to existing classes as well as share methods between them.

However, when we setup this relationship between parent and child classes, be aware that changing the behavior of the parent class can unknowingly cause problems for the child class. So try to keep classes as small as you can and be aware of the relationships you set up between our classes.

As a challenge to you, take a look at the documentation for the popular JavaScript library React at https://reactjs.org/ and see how it makes use of inheritance with normal JavaScript classes to give it different features.