/

dronkers.dev

dronkers.dev logo

Object Creation Patterns in JavaScript: 5 Benefits of Using ES6 Classes Over Constructors

2021-08-16

The ES6 standard introduced the class keyword as a new way to implement the pseudo-classical object creation pattern in JavaScript. Although the class keyword is often described as "syntactic sugar", it does provide a number of tangible benefits which I'd like to highlight for you below.

But first, a quick refresher on the pseudo-classical pattern using constructor functions (pre-ES6). From the book "JavaScript: The Definitive Guide" by David Flanagan:

A constructor function is a function designed for the initialization of newly created objects. Constructors are invoked using the new keyword. Constructor functions using new automatically create the new object, so the constructor itself only needs to initialize the state of that new object. The critical feature of constructor invocations is that the prototype property of the constructor is used as the prototype of the new object.

For example, let's say we're starting a bike shop and we need to write some custom code to add bikes as inventory to our records. Using constructor functions, we might implement something like the following:

function Bicycle (serialNumber, manufacturer, model, size) {
  this.serialNumber = serialNumber;
  this.manufacturer = manufacturer;
  this.model = model;
  this.size = size;
}

Bicycle.prototype.printSpecs = function() {
  console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
}

Bicycle.wheels = 2;

let xyzBike = new Bicycle('574726ABC', 'All-City', 'Space Horse', 55);

xyzBike.printSpecs(); // logs 'Item 574726ABC: All-City Space Horse. Size: 55'
console.log(xyzBike.hasOwnProperty('printSpecs')) // false

Above, we declare the constructor function Bicycle whose purpose is to initialize new Bicycle objects. Note that the function does not explicitly create or a return an object; rather, its purpose is simply to initialize this when used in conjunction with the new keyword.

Below the function declaration we define a new method printSpecs on the the object referenced by Bicycle.prototype. All objects created by the Bicycle constructor function will inherit from Bicycle.prototype via the prototypal chain and therefore be able to access the method printSpecs.

The statement Bicycle.wheels = 2 creates a "static" property, or a property of the constructor itself.

Lastly, Because we invoke the method printSpecs with the object xyzBike the method execution context (aka this) is bound to the calling object and outputs the desired information.

Whew, that can be a lot to take in. If you you're still with me, let's move on. If not, I highly recommend the following resources on prototypes in JavaScript:

Rewriting the Bicycle constructor function using the new class syntax looks something like this:

class Bicycle {
  constructor(serialNumber, manufacturer, model, size) {
    this.serialNumber = serialNumber;
    this.manufacturer = manufacturer;
    this.model = model;
    this.size = size;
  }

  printSpecs() {
    console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
  }

  static wheels = 2;
}

let xyzBike = new Bicycle('574726ABC', 'All-City', 'Space Horse', 55);

xyzBike.printSpecs(); // logs 'Item 574726ABC: All-City Space Horse. Size: 55'
console.log(xyzBike.hasOwnProperty('printSpecs')) // false

Can you notice the difference between the code snippets above? Judging from just these two examples, class isn't terribly exciting but bear with me. Before moving on, it's critical to understand that these two examples work in exactly the same way. The class keyword does not alter the fundamental nature of JavaScript's prototype-based classes, hence the descriptions of "syntactic sugar".

Ok now for the benefits the class keyword provides when compared to constructor functions:

**1) class provides a cleaner, more concise syntax for creating constructor functions and protoypes. **

Notice in the second example that the body of the class declaration defines two methods: constructor and printSpecs. The class body also defines a static property wheels. In this way we are effectively taking the first three statements from the first example and wrapping them in the class body. By using class syntax, we can consolidate the constructor function, prototype method assignments and static property assignments into one statement and make our intentions easy to understand.

**2) class enforces the use of the new keyword **

Constructor functions in most cases do not provide an explicit return value but rather rely on the keyword new to create a new object. If a developer were to mistakenly omit new when invoking a constructor function, save for other safeguards, things are likely to break:

function Bicycle (serialNumber, manufacturer, model, size) {
  this.serialNumber = serialNumber;
  this.manufacturer = manufacturer;
  this.model = model;
  this.size = size;
}

Bicycle.prototype.printSpecs = function() {
  console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
}

Bicycle.wheels = 2;

let xyzBike = Bicycle('574726ABC', 'All-City', 'Space Horse', 55); // new keyword omitted

xyzBike.printSpecs(); // TypeError: Cannot read property 'printSpecs' of undefined

Uh oh. JavaScript did't throw an error until we tried to invoke a property on our non-existent object. What would happen if we try to omit new when invoking a class?

class Bicycle {
  constructor(serialNumber, manufacturer, model, size) {
    this.serialNumber = serialNumber;
    this.manufacturer = manufacturer;
    this.model = model;
    this.size = size;
  }
  printSpecs() {
    console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
  }
  static wheels = 2;
}

let xyzBike = Bicycle('574726ABC', 'All-City', 'Space Horse', 55); // new keyword omitted

// TypeError: Class constructor Bicycle cannot be invoked without 'new'

That's much more helpful. Not only did JavaScript throw an error earlier, it also provided a more explicit message.

In short, by enforcing the use of new, classes help avoid silent fails.

3) class does not allow prototype methods to be defined with arrow functions

Unlike functions defined in other ways, Arrow functions inherit their execution context or value of the this keyword from the environment in which they are defined. This can be problematic in the context of constructor function methods. Extending our Bicycle constructor function example, let's re-write the printSpecs method with an arrow function:

function Bicycle (serialNumber, manufacturer, model, size) {
  this.serialNumber = serialNumber;
  this.manufacturer = manufacturer;
  this.model = model;
  this.size = size;
}

Bicycle.prototype.printSpecs = () => {
  console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
}

Bicycle.wheels = 2;

let xyzBike = new Bicycle('574726ABC', 'All-City', 'Space Horse', 55);

xyzBike.printSpecs(); // Item undefined: undefined undefined. Size: undefined

Not exactly what we were looking for was it? Because the printSpecs method is defined with arrow function syntax, this is bound to the global object , on which the properties we're trying to access do not exist.

Now, if we tried to do something similar using the class syntax, JavaScript will try to correct the mistake for us by pushing the method to each instance as an own property, in which case the arrow function invocation will have the correct context:

class Bicycle {
  constructor(serialNumber, manufacturer, model, size) {
    this.serialNumber = serialNumber;
    this.manufacturer = manufacturer;
    this.model = model;
    this.size = size;
  }
  
  printSpecs = () => {
    console.log(`Item ${this.serialNumber}: ${this.manufacturer} ${this.model}. Size: ${this.size}`);
  }

  static wheels = 2;
}

let xyzBike = new Bicycle('574726ABC', 'All-City', 'Space Horse', 55); // new keyword omitted

xyzBike.printSpecs(); // logs 'Item 574726ABC: All-City Space Horse. Size: 55'
console.log(xyzBike.hasOwnProperty('printSpecs')); // true

Here, the expected output is logged to the console but now JavaScript is loading every instance with a copy of the printSpec method. This is still not ideal but an improvement over the constructor function scenario nonetheless.

4) class automatically assigns a prototype.constructor property to subclasses

Let's say we wanted to create a 'Road Bike' sub-category of the Bicycle class. We can accomplish this by leveraging prototypal inheritance in JavaScript. Prior to ES6, however, the syntax was a bit clunky, especially when it came to the prototype.constructor property. For example:

function Roadbike(serialNumber, manufacturer, model, size, dropDistance) {
  Bicycle.call(this, serialNumber, manufacturer, model, size);
  this.dropDistance = dropDistance;
}

RoadBike.prototype = Object.create(Bicycle.prototype);

RoadBike.prototype.riderGreeting = function() {
  console.log("Leg's are shaved, let's pump out some watts!");
}

Above, we reassign the property RoadBike.prototype to the return value of Object.create(Bicycle.prototype) which establishes the prototypal chain between the "super class" (Bicycle) and the "sub class" (RoadBike). This allows instances of the RoadBike constructor to inherit methods from the superclass. But, we missed something. What if we need to lookup the origin of a road bike instance?

let fastBike = new RoadBike('998361XYZ', 'Specialized', 'Tarmac', 54, 125);
fastBike.printSpecs();
fastBike.riderGreeting();
console.log(fastBike.constructor); // Bicycle
console.log(Object.getPrototypeOf(fastBike).hasOwnProperty('constructor')); // false

Bicycle? Of course. Because we assign RoadBike.prototype to an empty object that inherits from Bicycle.prototype, JavaScript must look up the prototypal chain in order to find a constructor property. In order to make this more useful, we have to manually assign a constructor property to RoadBike.prototype.

RoadBike.prototype.constructor = RoadBike;

let fastBike = new RoadBike('998361XYZ', 'Specialized', 'Tarmac', 54, 125);

console.log(fastBike.constructor); // RoadBike
console.log(Object.getPrototypeOf(fastBike).hasOwnProperty('constructor')); // true

Using the class keyword makes this step unnecessary:

class RoadBike extends Bicycle {
  constructor(serialNumber, manufacturer, model, size, dropDistance) {
    super(serialNumber, manufacturer, model, size, dropDistance);
    this.dropDistance = dropDistance;
  }
  riderGreeting() {
    console.log("Leg's are shaved, let's pump out some watts!");
  }
}

let fastBike = new RoadBike('998361XYZ', 'Specialized', 'Tarmac', 54, 125);

console.log(fastBike.constructor); // RoadBike
console.log(Object.getPrototypeOf(fastBike).hasOwnProperty('constructor')); // true

Having prototype.constructor reflect the right constructor can be helpful for debugging and manual reassignment can easily be overlooked when using constructor functions. The class keyword makes for one less thing to worry about.

5) class allows for static property inheritance

Recall from earlier that we defined a static property 'wheels' on the Bicycle class. Under the constructor function pattern, the subclass RoadBike does not inherit the static properties defined on Bicycle.

function Bicycle (serialNumber, manufacturer, model, size) {
  this.serialNumber = serialNumber;
  this.manufacturer = manufacturer;
  this.model = model;
  this.size = size;
}

Bicycle.wheels = 2;

function RoadBike(serialNumber, manufacturer, model, size, dropDistance) {
  Bicycle.call(this, serialNumber, manufacturer, model, size);
  this.dropDistance = dropDistance;
}

RoadBike.prototype = Object.create(Bicycle.prototype);
RoadBike.prototype.constructor = RoadBike;

console.log(RoadBike.wheels); // undefined

But, you say, all Road Bikes have two wheels just as all Bicycles have two wheels. Wouldn't it make sense for the the RoadBike class to inherit the wheels static property? The class keyword solves that problem:

class Bicycle {
  constructor(serialNumber, manufacturer, model, size) {
    this.serialNumber = serialNumber;
    this.manufacturer = manufacturer;
    this.model = model;
    this.size = size;
  }
  
  static wheels = 2;
}

class RoadBike extends Bicycle {
  constructor(serialNumber, manufacturer, model, size, dropDistance) {
    super(serialNumber, manufacturer, model, size, dropDistance);
    this.dropDistance = dropDistance;
  }
}

console.log(RoadBike.wheels); // 2

Here, RoadBike inherits static methods and properties because RoadBike inherits from Bicycle. This is a special feature of the extends keyword and was not possible prior to ES6.

Thanks for reading! I hope this post helped your understanding of the class keyword in JavaScript.