Learning JavaScript Classes

Note: This post is work-in-progress learning-note and still in active development and updated regularly.

JavaScript is a Object Oriented language. Unlike other language (eg. C++, Java, Python etc) JS is not class-based nevertheless  Objects in JavaScript are considered everything. In ES2015 (ES6), a prototype-based class keyword was introduced. In this learning note we will deep dive into JS class, its common uses with a focus on its relation to prototypes.

Under the hood, ES6 classes are not something that is radically new: They mainly provide more convenient syntax to create old-school constructor functions.  Axel Rauschmayer

JavaScript classes are “special function” which can be defined as class expression or class declaration similar to to defining functions.

Function vs Class

In order to understand JS classes are special functions, lets create a simple Vehicle function using both function() syntax and class syntax below and examine differences between the two methods:

// Initialize Vehicle function
function Vehicle(make, model) {     //function declaration
  this.make= make;                  // define properties
  this.model= model;
}
//method declaration
Vehicle.prototype.reminder = function() {  //method declaration
  console.log(this.make);
}
//create myVehicle instance
let myVehicle = new Vehicle("Ford"); //create myVehicle instance (new' keyword)
myVehicle.reminder();                //Output => Ford

In the example above, we created a Vehicle() function (line 2)with two properties (make, model). Using object.prototype._ we created a reminder() method (line 7). Lastly, myVehicle object instance was created (line 11) using new keyword and expected output was displayed (line 12).

Prototype ( [[Prototype]] ) of vehicle() function can be accessed using the Object.getPrototypeOf() method as follows:

//access prototype of Vehicle function
Object.getPrototypeOf(Vehicle);
//ƒ () { [native code] }

Object.getPrototypeOf(myVehicle);
//{reminder: ƒ, constructor: ƒ}

With prototype-based language, any function can become a constructor instance using the new keyword (line 18).

In the example below, lets initialize the above Vehicle object with same properties and methods using class syntax.

// initialize Vehicle class
class Vehicle {               // class declaration
  constructor(make, model) {  //constructor declaration
    this.make = make;         // instance variables
    this.model= model;
  }
// method declaration
  reminder() {               // method declaration
    console.log(this.make);
  }
}
//Create myVehicle instance
let myVehicle = new Vehicle("Ford"); // object instance ('new' keyword)
myVehicle.reminder();                //OUTPUT => Ford

Lets also look at the vehicle() object created with class construct using the same Object.getPrototypeOf() method:

//access prototype of Vehicle class
Object.getPrototypeOf(Vehicle);
//ƒ () { [native code] }

Object.getPrototypeOf(myVehicle);
//{constructor: ƒ, reminder: ƒ}

Similar to the function, object instances created with new keyword also become a constructor instance as shown in the example above.

From the above two examples, we got the same output demonstrating that JS class are functions, but creating method with class keyword is much simpler (lines 8-10) and easier to read.

Defining Classes

Before defining objects using class keyword, lets revisit some terminologies that are used in JS classes.

  • Class: Classes are “special functions“, that are used to define objects properties & methods.
  • Object: It is a class instance and may include combination of data structure, variables and function.
  • Method: It’s function defined in a class object introduced in ES6
  • Constructor: The constructor is a special method for creating and initializing an object in a class. It is invoked when an object instance is created.
Class Declaration

In our previous example, we use vehicle object example. Lets use the same object to define using classes. Just like in function declaration, classes are declared as using class keyword followed by an identifier and code block inside {  } called class body.

Class Declaration Syntax

A class object can be defined by class declaration using class keyword followed by name of class. A simple class declaration syntax is shown below:

//initialize vehicle object
class Vehicle {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }
}
//retrive
console.log(Vehicle.name);
//OUTPUT => Vehicle

In the example above, in line 2 a Vehicle class was declared as class Vehicle { }. Unlike in function declaration, in class declaration constructor keyword is used to initialize object’s properties. Some key features of constructor function:

  • Class constructor, which initializes object instance) holds only method definitions and not data properties;
  • In ES6, methods are defined using shorter syntax (similar to getter and setter syntax of ES5);
  • Methods definitions in class bodies are NOT separated by ( ' ) comma;
  • Properties on class instance can be directly invoked as shown in line  ;
  • There can only be a single constructor method associated with a class.

Tip: Class declarations where extends keywords are not used to create sub-classes are known as base classes

In the following example (borrowed from Peleke Sengstacke’ article) the above Vehicle object expanded to include four properties and two methods toString() and reminder().

// vehicle is a base class
class Vehicle {
    constructor (make, year, color) {
        this.make = make;
        this.year = year;
        this.color = color;
    }
// create method toString
    toString () {
        return `Bring your ${this.color} ${this.make} (${this.year}) to dealer.`
    }
// create method reminder
    reminder () {
      console.log( this.toString() );
    }
}
//initialize myCar object instance
const myCar = new Vehicle('Toyota', 2018, 'black');
//Invoking object instance 
myCar.reminder(); //OUTPUT => Bring your black Toyota (2018) to dealer. 
console.log(myCar.color); //OUTPUT => Black

In the example above, a Vehicle object was initialize using class (line 2) was initialized to create myCar object instance using new keyword (line 19) with values ('Toyota', 2018, 'black') for the constructor function properties defined  (lines 3-7) as arguments (line 19). When the new object instance myCar was invoked, we got expected output (lines 12-13). Please take a note that Vehicle class property defined in (line ) can be directly referred with myCar instance as shown in output (line 21).

To quote from Peleke’s article, some key features of class declaration include:

  • Classes can only contain method definitions, not data properties;
  • When defining methods, you use shorthand method definitions;
  • Unlike when creating objects, you do not separate method definitions in class bodies with commas; and
  • You can refer to properties on instances of the class directly

Tip: Class declaration without extends keyword are known as base classes.

Class Expression

Classes are also initialized using class expression, where a class is assigned to a variable.  Just like in function expression, class expression can be named or unnamed. Since JS classes are prototype-based inheritance, if class expression is named, the name attribute of the class is local property to the class body only.

Class Expression Syntax

The following example shows basic syntax of defining a Vehicle object with class expression. This is similar to defining anonymous and named function expression.

// unnamed class
let Vehicle = class {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }
};
console.log(Vehicle.name);
// OUTPUT => Vehicle

// named class
let Vehicle= class Truck {
  constructor(make, model, year) { 
    this.make = make;
    this.model = model;
    this.year = year;
  }
};
//retrieve
console.log(Vehicle.name);
// OUTPUT => Truck

In the example above, Vehicle object was defined with class expression, just like a function is defined using function expression. The Vehicle object was assigned to a variable and class on the right side of assignment expression ( = ). Just like in functions, class expression can be anonymous (unnamed) (line 2) or with a name (eg. Truck) after class keyword (line 13). As MDN states, the name given to a named class expression is local to the class’s body and can be retrieved through the class’s (not an instance’s) .name property.

Lets revisit the Vehicle object that we defined in previous section with class declaration and define with class expression as shown below:

// initialize Vehicle class
let Vehicle = class {
  constructor (make, year, color) {
        this.make = make;
        this.year = year;
        this.color = color;
    }     
  //initialize method
   toString () {
        return `Bring your ${this.color} ${this.make}(${this.year}) to dealer.`
     }
//initialize method
   reminder () {
      console.log( this.toString() );
    }
};
 //initialize myCar instance with new keyword
let myCar = new Vehicle('Toyota', 2018, 'black');
//invoke myCar instance
myCar.reminder(); //OUTPUT => Bring your black Toyota (2018) to dealer.
console.log(myCar.color);  //OUTPUT => Black

The example above, an unnamed class was defined and assigned as to an Vehicle object variable using class expression syntax. As expected its output is exactly same.

Methods Definitions

Strict Mode

As MDN Describes it, the bodies of class declarations and class expressions are executed in strict mode i.e. constructor, static and prototype methods, getter and setter functions are executed in strict mode.

Prototype Method

These methods can be invoked as instance of an class object. It’s cleaner and more readable. Lets revisit the Vehicle class defined earlier under Class Declaration section.

//Using ES6 syntax 
//initialize Vehicle class
class Vehicle {
  constructor (make, model, year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }     
  //initialise method
   toString () {
        return `Bring your ${this.make} ${this.model} (${this.year}) to dealer.`
     }
//initialize method
   reminder () {
      console.log( this.toString() );
    }
};
//initialize myCar object instance
const myCar = new Vehicle('Toyota', 'Camry', 2018, 'black');
//invoke myCar
myCar.reminder(); //OUTPUT => Bring your Toyota Camry (2018) to dealer.

Before ES6, methods could only be defined using a constructor function. Its ‘prototype’ name comes from ES5 syntax which has prototype inheritance. Lets initialize below the same Vehicle class using ES5 syntax.

// Using ES5 syntax
// Initialize Vehicle class
function Vehicle (make, model, year)  {
        this.make = make;
        this.model = model;
        this.year = year;
    } 
// initialize methods
Vehicle.prototype.toString = function toString () {
    return `Bring your ${this.make} ${this.model} (${this.year}) to dealer.` 
};

Vehicle.prototype.reminder = function reminder () {
    console.log( this.toString() ); 
};

//initialize myCar object instance
const myCar = new Vehicle('Toyota', 'Camry', 2018, 'black');
//invoke myCar
myCar.reminder(); //OUTPUT => Bring your Toyota Camry (2018) to dealer.

The methods defined in Vehicle() function above is same as Vehicle class defined earlier. Syntax used in defining methods in ES5 is longer (lines: 9-15) compared to ES6 syntax.

With ES6, Accessor Descriptors (Getter and setter) can also be used to define prototype methods of class function.

//initialize Vehicle class
class Vehicle { 
  constructor (make, model) {
    this.make = make;
    this.model = model;  
  }     
  
  //initialise msg method
   message () {       
    return `Bring your ${this.make} ${this.model} to dealer.`    
  }
  //using get accessor method
  get reminder() {   
    return this.myReminder();
  }
  //initialize myRemindermethod
  myReminder () {   
   console.log( this.message() ); 
  }
};
//initialize myCar object instance
const myCar = new Vehicle('Toyota', 'Camry');
//invoke myCar.myReminder()
console.log(myCar.myReminder());
//OUTPUT
Bring your Toyota Camry to dealer.

In the example above, revisiting the Vehicle class function described above with get accessor descriptor method (line 13) we get the same exact output.

Note: More detail discussion about accessor descriptor properties are described in Understanding JavaScript Accessors: Getters & Setters learning-note post.

Constructor

As MDN describes it, the constructor method is a special method for creating and initializing an object created within a class. A typical constructor syntax:  constructor([arguments]) { ... }.

class Car {};
//access Car() with typeof
typeof Car; // 'function'

//initialize Car class
class Car (make, model) {
   this.make = make;
   this.model = model;
}

let myCar = Car("Toyota", "Corolla");
console.log(myCar); // SyntaxError


function Car (make, model) {
    this.name    = make;
    this.protein = model;
}

const myCar = Car('Toyota', 'Corolla'); 
console.log(myCar); // undefined

// creating new object instance with 'new'
let myVehicle = new Vehicle('Toyota', 'Corolla');
console.log(myVehicle); 
// OUTPUT => Vehicle {make: "Toyota", model: "Corolla"}

There can only be one constructor method in a class; more than one constructor in a class through a SyntaxError error.

Static Method

Static methods for a class can be created using static keyword. Static methods are similar to prototype methods except that they are called called by a class. These methods cannot be called with a class instance.

Static methods are commonly used to create utility functions on classes, for example methods that are common to all object.

//initialize Vehicle class
class Vehicle {              //class declaration
  constructor(make, model) {  // constructor declaration
    this.make = make;
    this.model = model;
  }
//add static method
 static reminder() {       // static method declaration
   console.log(`Bring your ${this.make} for tune up.`);
 }
}

//invoke static method
Vehicle.reminder();
//OUTPUT => Bring your undefined for tune up.

In the example above, static method reminder() was created ( line 8) on class Vehicle (line 2). The static method Vehicle.reminder() was called directly (line 14) on class Vehicle without class instantiation. Expected output ” Bring your undefined for tune up ” was displayed without any error.

Sub Classing with Extends

Class declarations or class expressions can be extended using extends keyword to create child classes often called subclasses or derived classes. In subclasses, an object defined in parent class can be shared with subclass and its properties can be modified or added new ones. Some key features of subclass, as summarized by Peleke include:

  • Subclasses are created from parent object using class keyword followed by another extends keyword (line ).
  • The super keyword should be used to refer to parent class properties
  • While referring to parental class, the constructor function in subclass should include parameters defined in parent class; can’t refer an empty constructor function in subclass.
  • The this keyword can be used only after defining super keyword in the constructor function.
// initialize Vehicle class
class Vehicle {                               
    constructor (make, model) {                    
        this.make = make;                        
        this.model = model;
    }
// define method
    run () {                                
        return `Fastest man alive ${this.model} & ${this.model}`;
    }
}
//initialize subclass Hybrid Vehicle with extends
class HybridVehicle extends Vehicle {              
    constructor(make, model, electric) {                  
        super(make, model);                        
        this.electric = electric;
    }
//define method
    fly () {                                
        return `I'm an alien ${this.model} & ${this.model} + ${this.electric}`;
    }
}
//initiaze myHybrid object instance with new keyword
const myHybrid = new HybridVehicle('Toyota','Camry','hybrid');         
console.log(myHybrid.run()); 
//OUTPUT => Fastest man alive Camry & Camry
console.log(myHybrid.fly()); 
//OUTPUT => I'm an alien Camry & Camry + hybrid

//output
myHybrid.run(); //OUTPUT=> "Fastest man alive Camry & Camry"

In the above example, we created a subclass named HybridVehicle using extends keyword from its parent class Vehicle object (line 12). The subclass constructor function was initialized referring to its properties from parent class and an additional electric variable (line 13). Properties from Vehicle (parent class) were referred using super keyword (line 14). The added electric variable added to the HybridVehicle subclass instance was defined using this.electric = electric (line 15).

Super Class Calls with Super

The super keyword is used to access and call functions on an object’s parent. When used in a constructor, the super keyword appears alone and must be used before the this keyword is used. The super keyword can also be used to call functions on a parent object.
The super keyword can be used to call methods from subclass (or parent class).

// Vehicle is a base class
class Vehicle {
    constructor (make, model, year, fuel) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.fuel = fuel;
    }
    toString () {
        return `${this.make} | ${this.model} ${this.year} ${this.fuel}`
    }
// initialize method
    reminder () {
      console.log( this.toString() );
    }
}
// HybridVehicle is a derived class
class HybridVehicle extends Vehicle {
    constructor (make, model, year) {
        super(make, model, year, 'hybrid');
    }
//initialize method
    reminder () {
        super.reminder(); 
        console.log(`Would you look at that -- ${this.make} has hybrid!`);
    }
}
//create new MyHybrid instance
const myHybrid = new HybridVehicle('Toyota', 'Camry', 2018);

//access
myHybrid.reminder(); 
//OUTPUT
Toyota | Camry 2018 hybrid
Would you look at that -- Toyota has hybrid!

In the example above, we call reminder() method (line )from parent class Vehicle (line ) using super keyword on HybridVehicle subclass (a child class)

Symbol & Mix-ins

The MDN Documentation describes two additional methods Symbol (or Species) and Mix-ins. These topics are important in deep diving JS class, however discussion of these advance topic is outside the scope of this basic JS class learning-post. These topics will be revisited as in a separate learning-note post.

Wrapping Up

In this learning-note, we covered classes (declaration),sub classing with extends, super class calls with super, symbol methods . How classes map to prototype-based code??

NEXT: Understanding JavaScript Class & Prototypes

Useful Resources & Links

While preparing this post, I have referred the following references extensively. Visit original link for more information.