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.