Classes and Inheritance: JavaScript ES6 Feature Series (Pt 8)

Paige Niedringhaus
ITNEXT
Published in
11 min readOct 27, 2019

--

Prototypes are still there… under the hood

Photo by Erik Mclean on Unsplash

Introduction

The inspiration behind these pieces is simple: there are still plenty of developers for whom JavaScript is a bit baffling— or at the very least, has some odd quirks in its behavior.

Despite that, JavaScript is responsible for powering almost 95% of the 10 million most popular web pages, according to Wikipedia.

Since it only continues to increase in usage and popularity, I wanted to provide a bunch of articles and examples of ES6+ features that I use regularly, for other developers to reference.

The aim is for these pieces to be short, in-depth explanations of various improvements to the language that I hope will inspire you to write some really cool stuff using JS. Who knows? You might even learn something new along the way. 😄

For my eight blog post, I’ll dive into JavaScript classes and class-based inheritance, the cleaner, more straightforward way to handle JavaScript’s existing prototype-based inheritance system.

Before there were classes, there were prototypes

I can’t talk about JavaScript classes without first talking about prototypes — the original mechanism by which JavaScript objects inherit features from each other.

It’s outside the scope of this blog do to a deep dive into the prototype object and how prototype-based inheritance works, but I’ll give you a brief overview.

Each object in JavaScript has a prototype object, which acts as a template that it inherits methods and properties from. That same prototype object may also have a prototype object that it inherits from as well, and so on, and so forth. This is referred to sometimes as the prototype chain.

When a property or method is called on an object, the browser first checks to see if the actual object has the method on it, if not it checks if the method is available to it through its prototype object. If the method’s not defined on the object’s personal constructor (where its prototype object comes from), the browser checks up another level to see if the constructor’s prototype object has the method available to it. The browser will do this all the way up until it reaches the object at the top with null as its prototype.

Hmm…?

Check out this example, this should help illustrate what I’m saying.

Example of traditional prototype-based inheritance

function Superhero(superName, realName, powers){
this.superName = superName,
this.realName = realName,
this.powers = powers
}
const wonderWoman = new Superhero('Wonder Woman', 'Diana Prince', 'Strength and flight');console.log(wonderWoman); /* Superhero {
superName: 'Wonder Woman',
realName: 'Diana Prince',
powers: 'Strength and flight' } */
Superhero.prototype.equipment = 'Lasso of truth';console.log(wonderWoman.realName); // Diana Princeconsole.log(wonderWoman.equipment); // Lasso of truthconsole.log(wonderWoman.catchPhrase); // undefinedconsole.log(wonderWoman.hasOwnProperty('equipment')); // falseconsole.log(Superhero.hasOwnProperty('equipment')); // falseconsole.log(Superhero.prototype.hasOwnProperty('equipment')); // true

Above in the example is a constructor called Superhero, which defines a new super-powered crime fighter.

I created wonderWoman, and gave her the properties of superName, realName, and powers. Then, I added a property called equipment to the Superhero's prototype object, with a value of 'Lasso of truth'.

When I called the object’s realName property, "Diana Prince” is found on the object itself. When I called equipment, "Lasso of truth” is found on the object constructor’s prototype. And when I try to call the property catchPhrase, it comes back as undefined because nowhere in the prototype chain does that property exist.

It’s proved by the lines where I check if either wonderWoman.hasOwnProperty() and Superhero.hasOwnProperty() of 'equipment', and both return false, but Superhero.prototype.hasOwnProperty('equipment') returns true.

That is JavaScript’s prototype-based inheritance in a nutshell. Keep in mind, this is what’s happening in the background when we’re using class-based syntax going forward.

Now let’s get on to the new ES6 classes.

Then came classes

Unlike other object oriented programming (OOP) languages like Java, which have always been class-based, JavaScript chose prototypes for its way of handling inheritance, but with the release of ECMAScript 2015, classes were introduced to the language’s syntax.

Let me be absolutely clear though: classes are just syntactical sugar over JavaScript’s existing prototype-based inheritance. They are NOT a new object-oriented inheritance model.

Classes are what might be defined as “special functions,” and just like with all functions, which I wrote about here, they can defined in two ways: class expressions and class declarations.

Class declarations

The first way to define a class is using a class declaration. To declare a class, you use the class keyword with the name of the class.

Example of a class declaration

class Book {
constructor(title, author) {
this.title = title;
this.author = author;
}
}
const novel = new Book('Moby Dick', 'Herman Melville');console.log(novel); // Book { title: 'Moby Dick', author: 'Herman Melville' }

One important thing to note is that unlike function declarations, class declarations ARE NOT hoisted. You must first declare your class, and then access it, otherwise you’ll get a ReferenceError thrown.

Class expressions

The other way to define a class is with a class expression. Class expressions can be named or unnamed. If a class expression is named, the name given to a named class expression is local to the class’s body.

Examples of class expressions, both unnamed and named

// unnamed class expression
let Drama = class {
constructor(title, author){
this.title = title;
this.author = author;
}
};
console.log(Drama.name); // Drama// named class expression
let Comedy = class Book2 {
constructor(title, author){
this.title = title;
this.author = author;
}
};
console.log(Comedy.name); // Book2

Ok, the basics of creating classes are out of the way, time to dive into the nitty-gritty details.

Class body & method definitions

Similar to functions once again, the body of the class is the part contained within the {}. In here is where the class members like the constructor and methods are defined.

Strict mode

The first thing you need to know is that the body of a class is always executed in strict mode.

All this means is code written here is subject to stricter syntax for increased performance, some otherwise silent errors will be thrown, and certain keywords are reserved for future versions of ECMAScript.

If you’ve been using any of the newer React, Angular or Vue frameworks, you’ve probably already been developing in strict mode by default, so it shouldn’t pose too much of a shift in your way of developing.

The constructor

Next up in the anatomy of a class is, potentially, a constructor. The constructor method is a special method for creating and initializing an object created with a class. There can only be one special method with the name "constructor" in each class. If a class contains more than one occurrence of a constructor method, aSyntaxError will be thrown.

A constructor can also use the super keyword to call the constructor of the super class, but I’ll get to that in more detail later in this post.

Prototype methods

Prototype methods can also be known as method definitions, and they’re a shorthand for a function assigned to the method’s name. These tend to make up the bulk of the class’s body.

Example of a prototype method on a class

class Book {
constructor(title, author){
this.title = title;
this.author = author;
}
publicizeBook() {
return `This book ${this.title} is written by renowned author ${this.author}.`;
}
}
const novel = new Book('Harry Potter', 'J.K. Rowling');console.log(novel.publicizeBook()); // This book Harry Potter is written by renowned author J.K. Rowling.console.log(Book.prototype.hasOwnProperty('publicizeBook')); // true

For this class Book, the method publicizeBook() is defined on it, so any objects created with the class of Book, will automatically also have the prototype method publicizeBook() available to them.

And check out the last line in the example. I check if the Book class owns the publicizeBook() method by testing if the Book's prototype object .hasOwnProperty('publicizeBook'), and it returns true. Which shows it’s actually prototype-based inheritance happening underneath at all.

Staring to make sense now?

Static methods

When you use the static keyword, it can be applied to either a method or a class. Static methods are called without instantiating their class and cannot be called through a class instance. These types of methods are often used to create utility functions for an application.

Example of a static method on a class

class Book {
constructor(title, author){
this.title = title;
this.author = author;
}
static youMightLike(title, similarTitle) {
return `If you like ${title}, you might also like ${similarTitle}.`
}
}
const novel = new Book('Moby Dick', 'Herman Melville');console.log(novel.youMightLike); // undefinedconsole.log(Book.youMightLike(novel.title, "A Midsummer Night's Dream")); // If you like Moby Dick, you might also like A Midsummer Night's Dream.

With this example, if you try to call the static method youMightLike() on the actual class-based object novel, you only get back an undefined value. If you call it, however, on the class Book, with two parameters, you’ll get back a response recommending one book based on another book.

Beware of boxing with prototype & static methods

When a method, either static or not, is called without a value for this assigned to it, the this value will be undefined inside the method. It’s because the body of the class always runs in strict mode regardless of whether it’s explicitly set or not.

Example of undefined ‘this’ inside a class

class Dog {
eat() {
return this;
}

static speak() {
return this;
}
}
let shibaInu = new Dog();
console.log(shibaInu.eat()); // Dog {}
let chowDown = shibaInu.eat;
console.log(chowDown); // undefined
console.log(Dog.speak.toString()); // Class Dog
let greet = Dog.speak;
console.log(greet); // undefined

If you write the above class in the traditional function-based syntax, then autoboxing in method calls will happen in non–strict mode based on the initial this value. If the initial value is undefined, this will be set to the global object.

Example of this taking the global scope if it’s not assigned the function

function Dog() { };Dog.prototype.eat = function() {
return this;
}
Dog.speak = function() {
return this;
}
let husky = new Dog();
console.log(husky.eat()); // Dog {}
let munch = husky.eat;
console.log(munch()); // global object (long list of available options)
let bark = Dog.speak;
console.log(bark()); // global object again (long list of available options)

Just be aware of this little difference between classes and functions, so it doesn’t unwittingly trip you up for a few hours.

Instance properties

Instance properties must be defined inside of class methods.

Example of defined instance properties inside the class

class Cat {
constructor(eats, sleeps){
this.eats = eats;
this.sleeps = sleeps;
}

knocksThingsOver(obj) {
return `Woops, the cat just knocked ${obj} over...`
}
}

While static data properties and prototype data properties must be defined outside of the ClassBody declaration:

Example defining static data & prototype data properties

Cat.plays = true;Cat.prototype.eatsCatnip = 'Goes nuts for it';

Field declarations (still experimental)

These next features: public and private field declarations are still in the experimental feature (stage 3), proposed at TC39, by the JavaScript standards committee. But they’re still worth knowing about as they’ll probably make it into the standard syntax sometime soon.

Native support in browsers is limited as of now, but the feature can be used through a build step with systems like Babel, courtesy of your Webpack config.

Public declarations

With the JavaScript field declaration syntax, the previous example of Cat can be written like so.

Example of public declarations in a class

class Cat {
eats = 'mice';
sleeps;
constructor(eats, sleeps){
this.eats = eats;
this.sleeps = sleeps;
}
knocksThingsOver(obj) {
return `Woops, the cat just knocked ${obj} over...`
}
}

By declaring fields up-front, class definitions become more self-documenting, and the fields are always present. It’s also cool that the fields can be declared with or without a default value, just like default values in functions, which I wrote about here.

Private declarations

Private declarations aren’t too different syntactically from their public counterparts. But they are slightly different in usage and execution.

Private fields can only be declared up-front in a field declaration. They cannot be created later through assigning to them, the way normal properties can.

It’s also an error to reference private fields from outside of the class; they can only be read or written within the class body. By defining things which are not visible outside of the class, you ensure that your classes’ users can’t depend on internals, which may change version to version.

Example of private declarations in a class

class Cat {
#eats = 'mice';
#sleeps;
constructor(eats, sleeps){
this.#eats = eats;
this.#sleeps = sleeps;
}
knocksThingsOver(obj) {
return `Woops, the cat just knocked ${obj} over...`
}
}

Like I said before, not widely in use yet, and I probably wouldn’t put it into production, but keep an eye out for these in future ES releases.

Sub classing with `extends`

The extends keyword is used in class declarations or class expressions to create a class as a child of another class.

Example of extending one class into a child as a sub class

class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise`);
}
}
class Horse extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(`${this.name} whinnies.`)
}
}
let thoroughbred = new Horse('Seabiscuit');
console.log(thoroughbred.speak()); // Seabiscuit whinnies.

The class Horse extends the other class Animal, to take in its methods, and since Horse also has its own constructor present, it needs to call super() before referencing this.

This syntax of extends is very similar to React’s class-based components, if you’ve ever worked with that framework.

React class-based component syntax

import React, { Component } from 'react';class Nav extends Component {
// ...do something
}

It’s also worth noting, when methods for a parent and child class are named similarly, the child’s method takes precedence over the parent’s method when calling it.

Super class calls with `super`

Now we’re back to that super keyword that’s been mentioned above a few times. The super keyword is used to call corresponding methods of super class. This is one distinct advantage over prototype-based inheritance, because technically, both methods from the parent and child class could be invoked just by using super instead of one or the other.

Example of accessing the parent class methods from the child class

class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise`);
}
}
class Hippo extends Animal {
speak() {
super.speak();
console.log(`${this.name} roars.`);
}
}
let hippo = new Hippo('Bertha');
console.log(hippo.speak()); // Bertha makes a noise.
// Bertha roars.

In this example, Hippo's speak() method actually calls both Animal's speak() method as well as its own unique one, by simply using super.speak() inside of Hippo's speak().

And that’s really all there is to super, it’s not so complicated once some examples are shown. Right?

And this, is really the most important parts about classes you’ll need to know while developing in JavaScript. Well done for making it to the end of this article! 😃

Conclusion

Classes have existed in other programming languages for decades, and JavaScript finally got on board (sort of) with the release ES 2015. Although classes in JavaScript look the same syntactically as other OOP languages, it’s still really prototype-based inheritance taking place under the hood.

This new way of thinking about methods and properties may take a little getting used to for all of us familiar with traditional prototype object, but classes provide some really great improvements to make our development lives easier, and I hope you’ll try them out to see for yourself.

My aim with this series is to deeply explain pieces of the ES6 syntax you use every day so you can use them for maximum impact and avoid common pitfalls.

Check back in a few weeks, I’ll be writing about more JavaScript and ES6 or something else related to web development, so please follow me so you don’t miss out.

Thanks for reading, I hope you’ll give ES6 class-syntax a chance in your JavaScript for easier prototype inheritance.

If you enjoyed reading this, you may also enjoy some of my other blogs in this series:

--

--

Staff Software Engineer at Blues, previously a digital marketer. Technical writer & speaker. Co-host of Front-end Fire & LogRocket podcasts