ES6 - A beginners guide - Classes
Stefan Wright
Posted on November 17, 2021
Hey!
It's been a while, I apologise! Work has been pretty hectic and I've not found the time for training, but... I am back! This time we are going to look at the introduction of Classes into ES6. Let it be said, I do not come from an OOP (Object Oriented Programming) background so this certainly isn't something that is common knowledge to me. If you see that I have missed something, or maybe misexplained please do reach out in the comments, let me know, and let's learn from each other. As usual, let's start with how we'd have been doing things in ES5...
The ES5 Way
You'll have seen this a lot if you're new to learning JavaScript in general, we'll use a Car in our examples
// This will create the ES5 version of a class
function Car(options) {
this.make = options.make;
this.model = options.model;
}
// We would then assign functions to the above Function
Car.prototype.drive = function () {
return "Vroom";
};
const car = new Car({make: "Ford", model: "Focus"});
console.log(car.make) //This would return Ford in a console log message
console.log(car.model) //This would return Focus in a console log message
console.log(car.drive()) // This would return the string Vroom in a console log message
The above is all well and good, and of course has sufficed for many years and is engrained in many JavaScript applications (including the one I currently support in my employment), my biggest objection here is that it is very "wordy" there is a lot of duplication of strings that just takes more time and is error prone.
Prototypal Inheritence in ES5
Inheritence is the process of taking one thing and expanding upon it for a secondary purpose. For example, we have the generic Car above, but let's say we wanted to expand that for a specific type of car, say a Supercar. Then what? let's expand upon the above
// This will create the ES5 version of a class
function Car(options) {
this.make = options.make;
this.model = options.model;
}
// We would then assign functions to the above Function
Car.prototype.drive = function () {
return "Vroom";
};
function Supercar(options) {
this.engineType = options.engineType
}
const supercar = new Supercar({engineType: "V8", make:"Ferrari", model: "458"});
console.log(supercar) //This would return {"engineType":"V8"} in a console log message
We have an issue here, the make and model parameters have gone missing, That's because they are not defined in Supercar but they are in Car. We need to inherit the parameters from Car if we want to show them
// This will create the ES5 version of a class
function Car(options) {
this.make = options.make;
this.model = options.model;
}
// We would then assign functions to the above Function
Car.prototype.drive = function () {
return "Vroom";
};
function Supercar(options) {
Car.call(this,options); // On it's own this will now change the output of supercar to include the make and model
this.engineType = options.engineType
}
Supercar.prototype = Object.create(Car.prototype); //This copies the prototype functions from Car so we can use them in Supercar
Supercar.prototype.constructor = Supercar;
Supercar.prototype.honk = function () {
return 'Beep'
}
const supercar = new Supercar({engineType: "V8", make:"Ferrari", model: "458"});
console.log(supercar) //This would return {"engineType":"V8", "make":"Ferrari", "model": "458"} in a console log message
console.log(supercar.drive()); // This would return 'Vroom' in a console log message
console.log(supercar.honk()); // This would return 'Beep' in a console log message
We have a lot of code here, it messy, and its confusing. That's because JavaScript was not designed to be an OOP language. To combat this, ES6 brought in the existence of Classes to try and bridge that gap, making it a little more familiar to OOP developers branching into JavaScript.
Let's look at refactoring into ES6
The idea of using a class is that we can bypass having to set up constructor functions, then setting up prototypes and inheritance, and cutting out a lot of the boilerplate code that was needed in ES5. Setting up a class is easy we simply use the class keyword and then the name of the class, followed by curly braces. Let's take a look:
class Car {
}
const car = new Car();
This gives us an empty class, we're on our way! Let's keep going with the refactoring
class Car {
drive() {
return 'Vroom';
}
}
const car = new Car();
console.log(car.drive()) // This would return 'Vroom' in a console log message
To add some initial data to the class, we would need to create a constructor object:
class Car {
constructor(options) {
this.make = options.make;
this.model = options.model
}
drive() {
return 'Vroom';
}
}
const car = new Car({make: "Ford", model: "Focus"});
console.log(car.drive()) // This would return 'Vroom' in a console log message
Isn't this already looking cleaner? So that gives us a refactored version of the very first ES5 block we wrote. Let's take a moment and note how this looks a lot cleaner, it's certainly easier to read, we don't have to keep writing Car.prototype
for our functions related to Car either. Let's move on to the inheritence part of the refactor to bring in our Supercar. I'll skip past the part of creating a second "base" class for Supercar and creating it's own function, I don't think we need to go into that:
class Car {
constructor(options) {
this.make = options.make;
this.model = options.model
}
drive() {
return 'Vroom';
}
}
class Supercar extends Car{
constructor(options){
super(options) // This line, allows us to inherit the constructor from the class we are inheriting/extending from (Car in our example)
this.engineType = options.engineType;
}
honk() {
return 'Beep';
}
}
const supercar = new Supercar({engineType: "V8", make:"Ferrari", model: "458"});
console.log(supercar) //This would return {"engineType":"V8", "make":"Ferrari", "model": "458"} in a console log message
console.log(supercar.drive()); // This would return 'Vroom' in a console log message
console.log(supercar.honk()); // This would return 'Beep' in a console log message
That's it! We created a Car class, we then used that class inside another class for Supercar, and now we can access properties and functions from Car, within Supercar. How awesome is that?!?!
The super
keyword
The super keyword is critical to our class extension if we want to utilise properties and functions in our secondary class from the primary class. Think of super as the "give me everything that they have" option. You can pass it parameters too as you see above for passing the options array, allowing the parent class to use make and model from our array. You can also use super to invoke a method from the parent class within the return statement of your second method, for example:
class Car {
constructor(options) {
this.make = options.make;
this.model = options.model
}
drive() {
return 'Vroom';
}
}
class Supercar extends Car{
constructor(options){
super(options) // This line, allows us to inherit the constructor from the class we are inheriting/extending from (Car in our example)
this.engineType = options.engineType;
}
drive() {
return `${super.drive()} Zoom`;
}
honk() {
return 'Beep';
}
}
const supercar = new Supercar({engineType: "V8", make:"Ferrari", model: "458"});
console.log(supercar) //This would return {"engineType":"V8", "make":"Ferrari", "model": "458"} in a console log message
console.log(supercar.drive()); // This would return 'Vroom Zoom' in a console log message
console.log(supercar.honk()); // This would return 'Beep' in a console log message
Whilst the above is slick, funky, fun I wouldn't advise it in a production environment, it will very likely lead to confusion, you end up needing stepping through so much more code when you try to extend functions and you will lose track of what is actually being called. In my opinion, keep super() at the constructor level.
Posted on November 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.