One place for hosting & domains

      Demystifying ES6 Classes And Prototypal Inheritance

      Introduction

      In the early history of the JavaScript language, a cloud of animosity formed over the lack of a proper syntax for defining classes like in most object-oriented languages. It wasn’t until the ES6 spec release in 2015 that the class keyword was introduced; it is described as syntactical sugar over the existing prototype-based inheritance.

      At its most basic level, the class keyword in ES6 is equivalent to a constructor function definition that conforms to prototype-based inheritance. It may seem redundant that a new keyword was introduced to wrap an already existing feature but it leads to readable code and lays the foundation upon which future object-oriented features can be built.

      Before ES6, if we had to create a blueprint (class) for creating many objects of the same type, we’d use a constructor function like this:

      function Animal(name, fierce) {
        Object.defineProperty(this, 'name', {
          get: function() { return name; }
        });
      
        Object.defineProperty(this, 'fierce', {
          get: function() { return fierce; }
        });
      }
      
      Animal.prototype.toString = function() {
        return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
      }
      

      This is a simple object constructor that represents a blueprint for creating instances of the Animal class. We have defined two read-only own properties and a custom toString method on the constructor function. We can now create an Animal instance with the new keyword:

      var Lion = new Animal('Lion', true);
      
      console.log(Lion.toString()); 
      

      Great! It works as expected. We can rewrite the code using the ES6 class for a concise version:

      class Animal {
        constructor(name, fierce) {
          this._name = name;
          this._fierce = fierce;
        }
      
        get name() {
          return this._name;
        }
      
        get fierce() {
          return `This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
        }
      
        toString() {
          return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
        }
      }
      

      Let’s create an instance of the Animal class with the new keyword as we did before:

      let Lion = new Animal('Lion', true);
      
      console.log(Lion.fierce);
      
      console.log(Lion.toString())
      

      Defining classes in ES6 is very straightforward and feels more natural in an object-oriented sense than the previous simulation using object constructors. Let’s take an in-depth look at the ES6 class by exploring some of its attributes and ramifications.

      Making a transition from using the older object constructors to the newer ES6 classes shouldn’t be difficult at all since the class keyword is just a special function and exhibits expected function behavior. For example, just like a function, a class can be defined by either a declaration or an expression, where the latter can be named or unnamed.

      A class declaration is defined with the class keyword and followed by the name of the class.

      class Animal {}
      

      We already used the class declaration when we wrote the ES6 version of the Animal constructor function :

      class Animal {
        constructor(name, fierce) {
          this._name = name;
          this._fierce = fierce;
        }
      }
      

      A class expression allows for a bit more flexibility; a class may be named or unnamed, however, when a class expression is named, the name attribute becomes a local property on the class’ body and can be accessed using the .name property.

      An unnamed class expression skips the name after the class keyword:

      
      let animal = class {}
      

      A named class expression, on the other hand, includes the name:

      
      let animal = class Animal {}
      

      When comparing the object constructor to the ES6 class, it is worthy of note that, unlike the object constructor that can be accessed before its scope is defined because of hoisting, the class can’t and isn’t hoisted.

      While this may seem like a major limitation on ES6 classes, it doesn’t have to be; good ES6 practice demands that if any function must mutate an instance of a class then it can be defined anywhere in the program but should be invoked only after the class itself has been defined.

      After defining a class using any of the two stated methods, the curly brackets {} should hold class members, such as instance variables, methods, or constructor; the code within the curly brackets make up the body of the class.

      A class’ constructor is simply a method whose purpose is to initialize an instance of that class. This means that whenever an instance of a class is created, the constructor (where it is defined) of the class is invoked to do something on that instance; it could maybe initialize the object’s properties with received parameters or default values when the former isn’t available.

      There can only be a single constructor method associated with a class so be careful not to define multiple constructor methods as this would result in a SyntaxError. The super keyword can be used within an object’s constructor to call the constructor of its superclass.

      class Animal {
        constructor(name, fierce) { 
          this._name = name;
          this._fierce = fierce;
        }
      }
      

      The code within the body of a class is executed in strict mode.

      Defining Methods

      The body of a class usually comprises instance variables to define the state of an instance of the class, and prototype methods to define the behavior of an instance of that class. Before ES6, if we needed to define a method on a constructor function, we could do it like this:

      function Animal(name, fierce) {
        Object.defineProperty(this, 'name', {
          get: function() { return name; }
        });
      
        Object.defineProperty(this, 'fierce', {
          get: function() { return fierce; }
        });
      }
      
      Animal.prototype.toString = function() {
        return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
      }
      

      Or

      function Animal(name, fierce) {
        Object.defineProperty(this, 'name', {
          get: function() { return name; }
        });
      
        Object.defineProperty(this, 'fierce', {
          get: function() { return fierce; }
        });
      
        this.toString = function() {
          return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
        }
      }
      

      The two different methods we defined above are referred to as prototype methods and can be invoked by an instance of a class. In ES6, we can define two types of methods: prototype and static methods. Defining a prototype method in ES6 is quite similar to what we have above, except that the syntax is cleaner (we don’t include the prototype property) and more readable:

      class Animal {
        constructor(name, fierce) {
          this._name = name;
          this._fierce = fierce;
        }
      
        get name() {
          return this._name;
        }
      
        get fierce() {
          return ` This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
        }
      
        toString() {
          return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
        }
      }
      

      Here we first define two getter methods using a shorter syntax, then we create a toString method that basically checks to see if an instance of the Animal class is a fierce or tame animal. These methods can be invoked by any instance of the Animal class but not by the class itself.

      ES6 prototype methods can be inherited by children classes to simulate an object-oriented behavior in JavaScript but under the hood, the inheritance feature is simply a function of the existing prototype chain and we’d look into this very soon.

      All ES6 methods cannot work as constructors and will throw a TypeError if invoked with the new keyword.

      Static methods resemble prototype methods in the fact that they define the behavior of the invoking object but differ from their prototype counterparts as they cannot be invoked by an instance of a class. A static method can only be invoked by a class; an attempt to invoke a static method with an instance of a class would result in unexpected behavior.

      A static method must be defined with the static keyword. In most cases, static methods are used as utility functions on classes.

      Let’s define a static utility method on the Animal class that simply returns a list of animals:

      class Animal {
        constructor(name, fierce){
          this._name = name;
          this._fierce = fierce;
        }
      
        static animalExamples() {
          return `Some examples of animals are Lion, Elephant, Sheep, Rhinoceros, etc.`
        }
      }
      

      Now, we can call the animalExamples() method on the class itself:

      console.log(Animal.animalExamples()); 
      

      In object-oriented programming, it’s good practice to create a base class that holds some generic methods and attributes, then create other more specific classes that inherit these generic methods from the base class, and so on. In ES5 we relied on the prototype chain to simulate this behavior and the syntax would sometimes become messy.

      ES6 introduced the somewhat familiar extends keyword that makes inheritance very easy. A subclass can easily inherit attributes from a base class like this:

      class Animal {
        constructor(name, fierce) {
          this._name = name;
          this._fierce = fierce;
        }
      
        get name() {
          return this._name;
        }
      
        get fierce() {
          return `This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
        }
      
        toString() {
          return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
        }
      }
      
      class Felidae extends Animal {
        constructor(name, fierce, family) {
          super(name, fierce);
          this._family = family;
        }
      
        family() {
          return `A ${this._name} is an animal of the ${this._family} subfamily under the ${Felidae.name} family`;
        }
      }
      

      We have created a subclass here — Felidae (colloquially referred to as “cats”) — and it inherits the methods on the Animal class. We make use of the super keyword within the constructor method of the Felidae class to invoke the super class’ (base class) constructor. Awesome, let’s try creating an instance of the Felidae class and invoking and own method and an inherited method:

      var Tiger = new Felidae('Tiger', true, 'Pantherinae');
      
      console.log(Tiger.toString()); 
      console.log(Tiger.family());   
      

      If a constructor is present within a sub-class, it needs to invoke super() before using “this”.

      It is also possible to use the extends keyword to extend a function-based “class”, but an attempt to extend a class solely created from object literals will result in an error.

      At the beginning of this article, we saw that most of the new keywords in ES6 are merely syntactical sugar over the existing prototype-based inheritance. Let’s now take a look under the sheets and see how the prototype chain works.

      While it’s nice to define classes and perform inheritance with the new ES6 keywords, it’s even nicer to understand how things work at the canonical level. Let’s take a look at JavaScript objects: All JavaScript objects have a private property that points to a second object (except in a few rare cases where it points to null) associated with them, this second object is called the prototype.

      The first object inherits properties from the prototype object and the prototype may, in turn, inherit some properties from its own prototype and it goes on like that until the last prototype on the chain has its prototype property equal to null.

      All JavaScript objects created by assigning an identifier the value of object literals share the same prototype object. This means that their private prototype property points to the same object in the prototype chain and hence inherits its properties. This object can be referred to in JavaScript code as Object.prototype.

      Objects created by invoking a class’ constructor or constructor function initialize their prototype from the prototype property of the constructor function. In other words, when a new object is created by invoking new Object(), that object’s prototype becomes Object.prototype just like any object created from object literals. Similarly, a new Date() object will inherit from Date.prototype() and a new Number() from Number.prototype().

      Nearly all objects in JavaScript are instances of Object which sits on the top of a prototype chain.

      We have seen that it’s normal for JavaScript objects to inherit properties from another object (prototype) but the Object.prototype exhibits a rare behavior where it does not have any prototype and does not inherit any properties (it sits on the top of a prototype chain) from another object.

      Nearly all of JavaScript’s built-in constructors inherit from Object.prototype, so we can say that Number.prototype inherits properties from Object.prototype. The effect of this relationship : creating an instance of Number in JavaScript (using new Number()) will inherit properties from both Number.prototype and Object.prototype and that is the prototype chain.

      JavaScript objects can be thought of as containers since they hold the properties defined on them and these properties are referred to as ” own properties” but they are not limited to just their own properties. The prototype chain plays a big role when a property is being sought on an object:

      • The property is first sought on the object as an own property
      • If the property isn’t found on the object, its prototype is checked next
      • If the property doesn’t exist on the prototype, the prototype of the prototype is queried
      • This querying of prototype after prototype continues until the property is found or until the end of the prototype chain is reached and an error is returned.

      Let’s write some code to clearly simulate the behavior of prototypal inheritance in JavaScript.

      We will be using the ES5 method — Object.create() — for this example so let’s define it:

      Object.create() is a method that creates a new object, using its first argument as the prototype of that object.

      let Animals = {};                 
      Animals.eat = true;               
      
      let Cat = Object.create(Animals); 
      Cat.sound = true;                 
      
      let Lion = Object.create(Cat);    
      Lion.roar = true;                 
      
      console.log(Lion.roar);           
      console.log(Lion.sound);          
      console.log(Lion.eat);            
      console.log(Lion.toString());     
      

      Here’s a verbal interpretation of what we did above:

      • We created an Animal object and it inherits properties from Object.prototype
      • We initialized Animal``'``s own property — eat — to true (all Animals eat)
      • We created a new object — Cat — and initialized its prototype to Animal (Therefore Cat inherits properties from Animal and Object.prototype)
      • We initialized Cat's own property — sound — to true (the animals under the cat family make sounds)
      • We created a new object — Lion — and initialized its prototype to Cat (Therefore Lion inherits properties from Cat, Animal and Object.prototype)
      • We initialized Lion's own property — roar — to true (Lions can roar)
      • Lastly, we logged own and inherited properties on the Lion object and they all returned the right values by first seeking for the properties on the Lion object then moving on to the prototypes (and prototypes of prototypes) where it wasn’t available on the former.

      This is a basic but accurate simulation of the prototypal inheritance in JavaScript using the prototype chain.

      In this article, we have gone through the basics of ES6 classes and prototypal inheritance. Hopefully, you have learned a thing or two from reading the article. If you have any questions, leave them below in the comments section.

      ES6 Modules and How to Use Import and Export in JavaScript


      While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or
      edited it to ensure you have an error-free learning experience. It’s on our list, and we’re working on it!
      You can help us out by using the “report an issue” button at the bottom of the tutorial.

      With ES2015 (ES6), with get built-in support for modules in JavaScript. Like with CommonJS, each file is its own module. To make objects, functions, classes or variables available to the outside world it’s as simple as exporting them and then importing them where needed in other files. Angular 2 makes heavy use of ES6 modules, so the syntax will be very familiar to those who’ve worked in Angular. The syntax is pretty straightforward:

      Exporting

      You can export members one by one. What’s not exported won’t be available directly outside the module:

      export const myNumbers = [1, 2, 3, 4];
      const animals = ['Panda', 'Bear', 'Eagle']; // Not available directly outside the module
      
      export function myLogger() {
        console.log(myNumbers, animals);
      }
      
      export class Alligator {
         constructor() {
           // ...
         }
      }
      

      Or you can export desired members in a single statement at the end of the module:

      export { myNumbers, myLogger, Alligator };
      

      Exporting with alias

      You can also give an aliases to exported members with the as keyword:

      export { myNumbers, myLogger as Logger, Alligator }
      

      Default export

      You can define a default export with the default keyword:

      export const myNumbers = [1, 2, 3, 4];
      const animals = ['Panda', 'Bear', 'Eagle'];
      
      export default function myLogger() {
        console.log(myNumbers, pets);
      }
      
      export class Alligator {
        constructor() {
          // ...
        }
      }
      

      Importing

      Importing is also very straightforward, with the import keyword, members to be imported in curly brackets and then the location of the module relative to the current file:

      import { myLogger, Alligator } from 'app.js';
      

      Importing with alias

      You can also alias members at import time:

      import myLogger as Logger from 'app.js';
      

      Importing all exported members

      You can import everything that’s imported by a module like this:

      import * as Utils from 'app.js';
      

      This allows you access to members with the dot notation:

      Utils.myLogger();
      

      Importing a module with a default member

      You import the default member by giving it a name of your choice. In the following example Logger is the name given to the imported default member:

      import Logger from 'app.js';
      

      And here’s how you would import non-default members on top of the default one:

      import Logger, { Alligator, myNumbers } from 'app.js';
      



      Source link