Javascript ES6 Classes - A More Intuitive Syntax To Prototypal Inheritance

Growing up I was a sucker for animal documentaries and I was fascinated with anything that had to do with lions, tigers, leopard etc... So no surprise if this exercise will have to do with them.

We will learn a little bit about the Panthera Genus of the Felidea family and some cool new syntax along the way

postimage

You can read about the Panthera lineage here:
https://www.wildcatfamily.com/panthera-lineage/

The Goal

So I want to setup a base model that will hold all the necessary properties and methods to create one of the seven types of big cats that we find in the Panthera lineage. The Panthera Genus is part of the Felidae family so in code it could look like this

The initial code

'use strict';  
const  Felidea = function() {  
  const bigCats = ['tiger','lion','jaguar', 'leopard', 'snow leopard'];
  const midSize = ['clouded leopard', 'sunda clouded leopard'];
  let species = Object.freeze([...bigCats, ...midSize]);
  return species;
}

const Panthera = function(kind, name, options) {  
  // check if the kind belong to the species list
  if (!kind || !Felidea().includes(kind.toLowerCase())) {
  let message = `The specified species: ${kind} does not belong to the family of Felidae.`;
    throw new Error(message);
  }
  this.kind = kind;
  this.name = name || 'Kitty Doe';
  this.options = options || {};
}

The Felidea function contains an array of all the species and just return that for now. In our refactor that will be a good excuse to look into static methods. I did not want to have to store the species array into the Panthera 'class' since I do not think it will be good design to have instances carry that general information.

So now when we run the code above we can create a new big cat

{  
    const stripes = new Panthera("tiger", "Stripes");
    console.log(stripes);
}

And we get the output below:

new Panthera instance

We can see that stripes is an instance of Panthera with the properties kind, name and options properly set. Looking at the instance prototype (__proto__) we can see that a constructor method is attached by default.

For the moment our dear Stripes can't do much so let's add a method to allow him to run. We could add a run function directly into the constructor, but then every instance of Panthera will have to carry that function around like it's own unique name and options object. So to avoid that we would place that method on the prototype

Panthera.prototype.run = function() {  
    console.log(`${this.name} is running!!!`);
}

We can run our previous code block and let's add a couple console statements

{  
    const stripes = new Panthera("tiger", "Stripes");
    stripes.run();
    console.log(stripes);
    console.log("Does name belong to stripes? ",stripes.hasOwnProperty('name'));
    console.log("Does run() belong to stripes? ",stripes.hasOwnProperty('run'));
}

Constructor function instance prototype

stripes can now call the run method, but looking at the output of the last two console statements we can see that if name is a property of it, run is not. This is the magic of the prototype chain at play here. If it does not find a property or method on the instance itself, it will travel up the chain until it finds one and use it, if not then it will throw a reference error.

We have now a solid base model, we can easily add more methods such as eat(), sleep(), top speed etc... Feel free to play with that. What I want to focus now is how to create a 'subclass' and essentially inherits everything from the base 'class'.

Creating a 'subclass'

Let's add a CloudedLeo object

const CloudedLeo = function(name, options) {  
  const kind = "clouded leopard";
  Panthera.call(this, kind, name, options);
}

Here since we are in a narrower context the only kind that could be created being a clouded leopard, we are setting it by default. Since we want to reuse the same functionalities as Panthera we make a call to it, which defaults to its constructor, to instantiate our new special leopard.

This seemingly works but we have not really created any type of relationship between the two objects, there is no inheritance. Moreover what is the point of creating a clouded leopard this way when it is just going to be another instance of Panthera, seems like extra code with no benefits.

To achieve true inheritance we need to essentially do two things:
1. First override the subclass prototype with the base class's
2. We have to reassign the subclass constructor to point back to itself in order to create true instances of the subclass

Confusing?? I bet! Let's see what it looks like in code

// step 1 and 2 and it has to be done in that order  
CloudedLeo.prototype = Object.create(Panthera.prototype);  
CloudedLeo.prototype.constructor = CloudedLeo;

I don't know about you, but this did not seem very intuitive to me at first. After many hours of research and endless of scratching my head I can highly recommend this ressource that reaaly will take you into a deep dive in understanding P.I.
OOP in Javascript: What You Need to Know

For the time being let's go back to our code and see what we can now do with our newly created subclass

{  
const stripes = new Panthera("tiger", "Stripes");  
// for clouded leo we only need to pass a name
const patches = new CloudedLeo("Patches");  
// they both have access to the run() method
stripes.run();  
patches.run();  
// let's look inside each
console.log(stripes);  
console.log(patches);  
}

Looking at the image of the output below we can note a few interesting things on CloudedLeo's instance

Sharing methods via the prototype chain

  1. The __proto__ object is set to Panthera
  2. The constructor was reassigned back to the current subclass
  3. The run() method only exists on the Panthera instance, so when it was not found on the 'subclass' it went one level deeper and found it there.

To finalize this exercise before the refactor, let's override the run() method and customize for the CloudedLeo object. I want to use the base 'class' message and append a new one to it.

CloudedLeo.prototype.run = function() {
     // Call parent run method
    Panthera.prototype.run.call(this);
    // add custom message
    console.log(The top speed of a ${this.kind} is 64mph); 
 }

After saving the above and running the same code again, we see that the call to run() is now pointing to the CloudedLeo prototype. On top of that Chrome Dev Tools adds a convenience property calledd [[FunctionLocation]], clicking on it shows you exactly where the call will be executed. Very handy :)

Preview of Prototypal Inheritance

In Summary

That was a long walk through and hopefully it highlights some of the pain tied to the old syntax. I personally do not find it intuitive and I would not have the pretense to say that I have fully mastered it.
Now let the healing begin and see how we would refactor this using the new syntax

The (re)Factor

We will start with our Felidea object that was essentially only returning an array of accepted species. In the class architecture this does not need to be instatiated and we could return the array via a static method

// like functions we can use a class declaration  
class Felidea{  
    /*
     * we wrap our previous code inside a species method with a static keyword
     * in front to show that there is no need of instatiation with 
     * the new keyword
    */
    static species() {
        const bigCats = ['tiger','lion','jaguar', 'leopard', 'snow leopard'];
        const midSize = ['clouded leopard', 'sunda clouded leopard'];
        const species = [...bigCats, ...midSize];
        return species;
    }
}

This syntax

 //static species()

is the exact equivalent of

static: function() 

This is a method shorthand definition and you will also encounter something similar inside object literals in ES6 with only one big difference:

Inside a class you cannot use a comma to separate multiple methods whereas inside of an object it will throw an error if you don't have the commas. You could use a semicolon inside a class as a separator to future proof it but omitting is safe

/*  
 * this works
 */
 methodOne() {
     // code here
 }

 methodTwo() {
    // code here
 }

/*
 * this will throw a syntax error
 */
 methodThree() {
     // code here
 }, //comma not allowed as separator inside of class body
 methodFour() {
    // code here
 }

Ok now that we have that out of the way if we run our Felidea class as a standalone we can call the species() method directly as such

console.log(Felidea.species());

We get the output below:
Calling a static method - Javscript ES6 classes

Now let's rewrite the Panthera 'class' with the code below

class Panthera {  
  // check if the kind belong to the species list
  constructor(kind, name, options) {
    if (!kind || !Felidea.species().includes(kind.toLowerCase())) {
    let message = `The specified species: ${kind} does not belong to the family of Felidae.`;
      throw new Error(message);
    }
    this.kind = kind;
    this.name = name || 'Kitty Doe';
    this.options = options || {};
  }

  run() {
    return `${this.name} is running!!!`;
  }
}

Most of the code is the same, the major change is that we have wrapped the core explicitly into a constructor() method and we are setting up the run() method directly inside the body of the class declaration. We could still make a call to the prototype to set the additional methods and there migh be use cases for it, but this feels more contained to me.

There are three things to retain here:
1. Constructor has to be explicitly set on a base class
2. Using this synstax a class cannot be invoked like a function

Panthera();
/* will throw
 * Uncaught TypeError: Class constructor Panthera 
 * cannot be invoked without 'new'at...
 */
3. Last but not least (not obvious), class declarations are not hoisted to the top, so order matters

Running this code will work perfectly as you can see in the first part of the output below

Class structure on top - prototype structure below

The second part of the output have a a few interesting facts that reveal that indeed it's plain old functions and protoypes running under the hood:

  1. typeof Panthera is a "function"
  2. Since it is a function it has a prototype object where the methods get attached
  3. The constructor is of type class. This type is the only thing different from the constructor function we looked at earlier.

Though it's a coat on top of the old syntax, I really like how this reads out. I like it even more so with this step coming up - adding a subclass

Extending a class

The syntax is pretty straightforward to extend a base class. Repeating the CloudedLeo example from earlier, here is the new code

class CloudedLeo extends Panthera {
        constructor(name, options) {
            const kind = "clouded leopard";
            super(kind, name, options);
            this.nickname = sunny ${this.name};
          }
        run() {
            console.log(${super.run()} The top speed of a ${this.kind} is 64mph);
        }
}

That's essentially all there is to it. Let walk through the code and see what is important to retain

  1. ES6 introduces the extends keyword to establish the relationship between the subclass and the parent
  2. The super keyword points to the parent class and makes it convenient to access the parent constructor and other methods
  3. If your subclass contains it's own constructor, a call to super() has to be made before you are able to use the keyword this. Not doing so will throw a reference error.

Let's run with this new piece of code and see if it runs the same as our previous functional structure

{  
    const stripes = new Panthera("tiger", "Stripes");
    console.log(stripes.run());
    console.log(stripes);
    const patches = new CloudedLeo("Patches");
    patches.run();
    console.log(patches);
}

Looking at the output above we can see that the functionality remained the same at the cost of a less verbose and more intutive syntax, at least in my humble opinion :)

As a reference below are the complete code of the initial and refactored pieces

With Conctructor Function

const  Felidea = function() {  
  const bigCats = ['tiger','lion','jaguar', 'leopard', 'snow leopard'];
  const midSize = ['clouded leopard', 'sunda clouded leopard'];
  const species = Object.freeze(bigCats.concat(Array.prototype.slice.call(midSize)));
  return species;
}

const Panthera = function(kind, name, options) {  
  // check if the kind belong to the species list
  if (!kind || !Felidea().includes(kind.toLowerCase())) {
    /*
    let message = 'The specified species: ' + kind + 'does not belong to the family of Felidae.';
    */
  let message = `The specified species: ${kind} does not belong to the family of Felidae.`;
    throw new Error(message);
  }
  this.kind = kind;
  this.name = name || 'Kitty Doe';
  this.options = options || {};
}

Panthera.prototype.run = function() {  
  console.log(`${this.name} is running!!!`);
};

const CloudedLeo = function(name, options) {  
  const kind = "clouded leopard";
  Panthera.call(this, kind, name, options);
}

CloudedLeo.prototype = Object.create(Panthera.prototype);  
CloudedLeo.prototype.constructor = CloudedLeo;

CloudedLeo.prototype.run = function() {  
  // Call parent run method
  Panthera.prototype.run.call(this);
  // add custom message
  console.log(`The top speed of a ${this.kind} is 64mph`); 
 }

Refactored with Class Structure

class  Felidea {  
  static species() {
    const bigCats = ['tiger','lion','jaguar', 'leopard', 'snow leopard'];
    const midSize = ['clouded leopard', 'sunda clouded leopard'];
    const species = [...bigCats,...midSize];
    return species;
  }

}

class Panthera {  
  // check if the kind belong to the species list
  constructor(kind, name, options) {
    if (!kind || !Felidea.species().includes(kind.toLowerCase())) {
    let message = `The specified species: ${kind} does not belong to the family of Felidae.`;
      throw new Error(message);
    }
    this.kind = kind;
    this.name = name || 'Kitty Doe';
    this.options = options || {};
  }

  run() {
    return `${this.name} is running!!!`;
  }
}

class CloudedLeo extends Panthera {  
  constructor(name, options) {
    const kind = "clouded leopard";
    super(kind, name, options);
    this.nickname = `sunny ${this.name}`;
  }
  run() {
    console.log(`${super.run()} The top speed of a ${this.kind} is 64mph`);
  }
}

Final Words

As with anything new, you will have adopters, skeptics and downright hell no people around it. This is not the battlefield (I mean post) for that. If you like what you have seen so far, play around with it, start small and go gradually in depth with it. Pitfalls and limitations there will be as with anything else.

I personally like the fact that I can just look at the code an pretty much figure out what is going on in an almost natural way.

Let me know in the comments how you feel about it and if you have been using it in your Vanilla JS structure.

Ady Ngom

Ady Ngom

http://adyngom.com

Ady Ngom is a freelance web and mobile application developer who has a passion for well crafted interfaces. You might catch him humming a good tune while taking long walks with a camera in hands.

View Comments
Navigation