ES6 Classes
The following blog post is part of our ECMAScript 2015+ series of tutorials. In this part, we will ease into ES6 Classes by giving you a practical example for game User Interface. It is important to note that Coherent GT 2.0, which will be released at the end of this month, supports ES6 Classes.
What is an ES6 Class
Classes are introduced in ECMAScript 2015. The JS class
introduces a more common class syntax compared to other object-oriented programming languages. The class
syntax is a syntactic sugar over the existing prototype-based inheritance. The syntax also provides newcomers with more understandable and simpler way to achieve the same results without using the sometimes confusing prototype
syntax.
There are two ways to define a class by using either class declarations or class expressions.
Class declaration
1 2 3 |
class Animal { // class body } |
Class expression – named or unnamed
Named:
To refer to the class inside of the class body, you have to create a named class expression.
The name is local to the class body {}
– it will be visible only in the scope of the class expression.
1 2 3 |
let Animal = class Animal { // class body }; |
Unnamed:
1 2 3 |
let Animal = class { // class body }; |
Note: You can only access a class after you have declared it as the classes are not hoisted.
ES6 Class methods
The ES6 Classes have three method types – constructor, static and prototype.
Constructor method
The constructor method creates and initializes an object within a class.
Only one constructor method is allowed within a single class. As it is optional, if you do not specify one, a default constructor is used.
1 2 3 4 5 6 |
class Animal { constructor(name, age) { // constructor method this.name = name; this.age = age; } } |
Prototype method
A method, accessed using the instance of the class, is known as a prototype method. These methods can be inherited and used with objects of the class. The keyword this
inside a prototype method is relative to the respective object (instance).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Animal { constructor(name, age) { this.name = name; this.age = age; } getAnimalInfo() { // prototype method console.log(this.name + " is " + this.age + " years of age."); } } let dog = new Animal("dog", 7); dog.getAnimalInfo(); // dog is 7 years of age. |
dog.__proto__ === dog.constructor.prototype
dog.constructor.prototype === Animal.prototype
Static method
Prefixing a method definition with static
creates a static method. A static method is assigned to the class, not to its prototype. The method cannot be directly called on at instances of the class.
In other words – a static method cannot access data stored in specific objects and can be called from the class itself. If you do not need a method to belong to any particular object, a static method is the way to go.
Let’s look at the example below, which includes the three class methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Animal { constructor(name, age) { // constructor method this.name = name; this.age = age; } prototypeMethod() { // prototype method console.log(this); // `this` keyword references the instance of the class } static staticMethod() { // static method console.log(this); // 'this' keyword references the class } } let dog = new Animal("dog", 6); dog.prototypeMethod(); Animal.staticMethod(); |
A static method is generally used to create utility functions with common functionality that can be used anywhere in the application. We will show you an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Car { constructor(model, horsepower) { // constructor method this.model = model; this.horsepower = horsepower; } carInfo() { // prototype method console.log(this.model + " with " + this.horsepower + " engine horsepower."); } static powerToWeightCalc(horsepower, weight) { // static method const ptwResult = (horsepower / weight).toFixed(3); const ptwResultPerKg = (horsepower / (weight * 0.45359237)).toFixed(3); // console.log("Power to Weight Ratio: " + ptwResult + "HP per lb." + " / " + ptwResultPerKg + "HP per kg."); } } let koenigsegg = new Car("Regera", 1500); koenigsegg.carInfo(); // Regera with 1500 engine horsepower. Car.powerToWeightCalc(1000, 1000); // Power to Weight Ratio: 1.000HP per lb. / 2.205HP per kg. Car.powerToWeightCalc(koenigsegg.horsepower, 3589); // Power to Weight Ratio: 0.418HP per lb. / 0.921HP per kg. |
There is no reason to pollute the created instances of the class with another prototype method if it is not critical for the instance to own it.
Inheritance (extends
keyword)
The option class
comes with a way to assist with the prototype syntax and prototype inheritance.
The ability to inherit a method from a parent class is as easy as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Vehicle { constructor(fuelEconomy, seats) { this.fuelEconomy = fuelEconomy; this.seats = seats; } getFuelEconomy() { // parent method console.log("Miles per Gallon: " + this.fuelEconomy + "."); console.log("Litres per 100 Kilometres: " + (235.21 / this.fuelEconomy).toFixed(1) + "."); } } class Car extends Vehicle { // "Car" is now a subclass of Vehicle constructor(fuelEconomy, seats = 4, brand) { super(fuelEconomy, seats); // super() must be called because "Car" has a constructor also this.brand = brand; } } let car = new Car(25, 2, "Koen"); car.getFuelEconomy(); // calls the inherited method from "Vehicle" // Miles per Gallon: 25. // Litres per 100 Kilometres: 9.4. |
In this case super(fuelEconomy, seats)
calls the parent constructor and adds those properties to the newly created instance.
the super keyword
As mentioned above, the super()
calls the parent (class) constructor.
super()
must be used if a subclass has a constructor method. Also, it needs to be used before this
– it is not allowed before superclass constructor invocation.
With super
we can also call static methods of the parent class:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Word { static openKeyword() { return 'Open '; } } class MagicalWord extends Word { static sayAllTheMagicWords() { console.log(super.openKeyword() + 'Sesame!'); } } MagicalWord.sayAllTheMagicWords(); // Open Sesame! |
MagicalWord.sayAllTheMagicWords()
calls the inherited static method openKeyword()
from the class Word
.
Game User Interface Example
Lets’ do Profile, Inventory and Weapon classes to populate the Inventory with
weapons.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
const allItemsAvailable = 24; const itemTypes = ['Pistol', 'Machine gun', 'Sniper']; class Profile { constructor(name, credits, tokens) { this.name = name; this.credits = credits; this.tokens = tokens; } getBalance() { // used every time after a purchase console.log("Credits: " + this.credits + ", Tokens: " + this.tokens + "."); } } class Inventory { constructor(resources = []) { this.resources = resources; } static initList() { for (let i = 0; i < allItemsAvailable; i++) { inventory.resources.push(new Weapon( itemTypes[Math.floor(Math.random() * 3)], Math.floor(Math.random() * 5000), Math.floor(Math.random() * (3500 - 1000) + 1000), Math.floor(Math.random() * (300 - 50) + 50), Math.floor(Math.random() * 50) )); } } } class Weapon { constructor(name, type, cost, damage, rof, rounds) { this.name = name; this.type = type; this.cost = cost; this.damage = damage; this.rof = rof; this.rounds = rounds; } static damagePerSecond(damage, rof) { console.log("This weapon has " + ((rof / 60) * damage).toFixed(2) + " DPS."); } } let profile = new Profile("Swarmer", 51942, 3895); profile.getBalance(); // Credits: 51942, Tokens: 3895. let inventory = new Inventory(); Inventory.initList(); Weapon.damagePerSecond(100, 600); // This weapon has 1000.00 DPS. Weapon.damagePerSecond(inventory.resources[0].rof, inventory.resources[0].damage); |
and the last output – This weapon has X DPS. -> where X is a randomly generated value.
As you can see the damagePerSecond
method can also be used with a given weapon.
Conclusion
Although there are some disputes in the JavaScript community if classes are a good or a bad thing, you can clearly see that they come with benefits. ES6 Classes are a good guide to code unification.
The class syntax is on one level, a syntactic sugar over the prototype syntax, and it can help you write code that is less prone to error. The code looks simpler, cleaner and more readable. Constructors and static properties are subclassable out of the box while inheritance is build-in. Since classes are build-in and there is one way to use them, there is no need for libraries and the code becomes portable between frameworks.
For more interesting information regarding ES6 Classes and their possibilities follow @CoherentLabs on Twitter or start a discussion in our Forum.
Good reads:
Object-Oriented JavaScript — A Deep Dive into ES6 Classes
What’s the Difference Between Class & Prototypal Inheritance?