Class - ES6

Summary

As we all know Javascript is a prototype based language and unlike other object-oriented programming languages, it does not support classes and classical inheritance in ES5 and previous ECMAScript versions. class-like structure has been requested by many developers and now it is introduced in ES6. Many developers(including me) feel strongly that the language doesn't need classes, because essentially it is not a class-based language and adding classes will obscure the prototype principle behind Javascript, but class does lead to a much more concise syntax and organize the code in a more structured way. IMHO, even though we have the class syntax, we still need to understand that class structure in Javascript is only a syntax sugar and it is essential to understand the prototype principle behind the language.

Syntax

class structure in ES5

Let's use an example to illustrate how we define a custom type:

/** Constructor **/
function Square(length) {
    this.length = length;
}

// functions are usually defined in its prototype
Square.prototype.getArea = function() {
    return this.length * this.length;
}

In this example, we define a constructor in a normal function way and add a method getArea to its prototype.

Class declaration

In ES6, a new keyword class is introduced, we can rewrite the above example like this:

class Square {
    constructor(length) {
        this.length = length;
    }
    
    getArea() {
        return this.length * this.length;
    }
}

In this way, we get rid of the prototype on the surface and make the syntax cleaner, but again we need to keep in mind that this is only a syntax sugar and the methods are still defined in the prototype.

Why use the Class Syntax

We need to keep some important differences between ES5 and ES6 syntax in mind:

  • Class declarations, unlike function declarations, are not hoisted, just like let and const.
  • All code inside class declarations runs in strict mode automatically and there's no way to opt out of strict mode.
  • All methods defined inside class are nonenumerable while methods defined in function declarations are all numerable.
  • Attempting to overwrite a class name inside a method throws an error.
  • Calling a class constructor without new throws an error.
  • Class declaration will not be hoisted!!!

With those differences in mind, we can rewrite the ES6 class in ES5:

let Square = (function() {
    'use strict';
    const Square = function(length) {
        if (typeof new.target === 'undefined') {
            throw new Error('Constructor must be called with new.');
        }
        this.length = length;
    }
    
    Object.defineProperty(Square.prototype, 'getArea', {
        value: function() {
            // make sure this function is not called with new
            if (typeof new.target !== 'undefined') {
                throw new Error('Method cannot be called with new');
            }
            return this.length * this.length;
        },
        enumerable: false,
        writable: true,
        configurable: true,
    });
    
    return Square;
}());

In this example, we use let in the outmost scope and const inside to declare Square, the reason behind this is because class name is not allowed to modify by class methods while could be modified outside the class. We also use the Object.defineProperty to define the getArea() method to make it nonenumerable. The final step returns the constructor.

Class Expression

Except from class declaration, we can also use class expression to define a class:

const Square = class {
    constructor(length) {
        this.length = length;
    }
    
    getArea() {
        return this.length * this.length;
    }
}

And of course we can also use named class declaration:

const Square = class Square2 {
    // rest of the code
}

Class as First-Class Citizens

What is First-Class Citizens in programming? If a value can be passed as a parameter in a function, returned from a function and assigned to a variable, then it is a First-Class Citizen. Class in ES6 is a first-class citizen just like functions. For example:

function createObj(classDef) {
    return new classDef();
}

// pass class definition to a function
const squareObj = createObj(class {
    // class body
});

// return a class definition
function returnSquareClass() {
    return class {
        // class body
    };
}

// assign to a variable, class expression is an example
const Square = class {
    // class body
}

Accessor Properties & Computed Member Names

Just like functions, we can define accessor properties in class:

let propertyName = "parentNode";
class CustomElement {
    constructor(element) {
        this.element = element;
    }
    
    get html() {
        return this.element.innerHTML;
    }
    
    set html(value) {
        this.element.innerHTML = value;
    }
    
    [propertyName]() {
        return this.element.parentNode;
    }
}

We use get and set to define a accessor property in CustomElement class definition as well as computed values enclosed by square brackets.

Static Members

We can use static before a method name to make it a static method in class. For static variables, we can use the similar way as ES5 syntax.

class Square {
    // static method
    static create() {
        return new Square();
    }
}
// static variable
Square.name = 'SQUARE';

Inheritance

Inheritance in ES5

Let's use an example to illustrate the expensive process to implement inheritance in ES5.

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
}

function Square(length) {
    Rectangle.call(this, length, length);
    // we can define extra variables here
    this.name = 'SQUARE';
}

// let Square inherits all methods defined in Rectangle.prototype
Square.prototype = Object.create(Rectangle.prototype);

// Set Square.prototype.constructor points to Square constructor, otherwise it will point to Rectangle which is problematic.
Square.prototype.constructor = Square;

// Optionally, we can define extra methods in Square.prototype
Square.prototype.getName = function() {
    return this.name;
}

Inheritance in ES6

In ES6, we can use keyword extends to make the process much easier and here is the equivalent of the preceding example.

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    
    getArea() {
        return this.length * this.width;
    }
    // static method
    static create(length, width) {
        return new Rectangle(length, width);
    }
}

Rectangle.staticProp = "Rectangle";

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
        this.name = "SQUARE";
    }
    // overwrite super class method
    getArea() {
        // some strange definition to get area
    }
    
    // extra methods defined in subcalss
    getName() {
        return this.name;
    }
}

const squareObj = new Square(5);
console.log(squareObj.getArea());  // 25
console.log(squareObj.getName());  // SQUARE

const rect = Square.create(3, 4);
console.log(rect.strangeProp);  // Rectangle

In ES6 syntax, we use extends to explicitly inherits from Rectangle and calling super() in the constructor to invoke parent class constructor. There's one thing we need to bear in mind that if we specify the constructor in derived class, super() is required inside the constructor. In terms of the static members in super class, they are all available to subclasses, but we probably want to overwrite them if we want to have subclass specific properties.