JavaScript classes were introduced with ECMAScript 2015, they’re often described as syntactical sugar over JavaScript’s existing structure of prototypical inheritance. So while classes do not introduce a new inheritance model to JavaScript — they do provide syntactical simplicity. This simplicity can help us produce less error-prone & more readable code.
Classes are just like Functions
Classes are very similar to functions. Much like functions that have both function expressions and function declarations, classes have two components: class expressions and class declarations.
Class Declarations
Let's take a look at how we can define a class using a class declaration. We use the class
keyword followed the name of the class:
class Image {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
Hoisting
One important difference between function declarations and class declarations is that function declarations are hoisted and class declarations are not. You first need to declare your class and then access it, otherwise, code like the following will throw a ReferenceError
:
const p = new Image(); // ReferenceError
class Image{}
Class Expressions
A class expression is the other way to define a class. Class expressions can be named or unnamed. Note that the name given to a named class expression is local to the class’s body. (So it’s retrievable through the classes name
property):
// An unnamed class expression
let Image = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Image.name);
// output: "Image"
// A named class expression
let MyImage = class Image {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(MyImage.name);
// output: "Image"
Note: Class expressions are subject to the same hoisting restrictions as described previously in the Class declarations section.
Constructors
The constructor
method is a special method within JavaScript that we use to create and initialize an object created with a class
. We can only use one method with the name "constructor" within a class.
Our constructor can use the super
keyword to call the constructor of the superclass (more on this later in the article!).
Instance Properties
Instance properties must be defined inside of our class methods:
class Image {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
If we wish to use static class-side properties and prototype data properties, these must be defined outside of the classes body declaration:
Image.staticWidth = 50;
Image.prototype.prototypeWidth = 55;
Field Declarations
Whilst the syntax is still considered experimental (it’s not yet adopted by many browsers), public and private field declarations are also worth knowing about — as often you’ll be developing with a Babel which will transpire the syntax for you.
Public Field Declarations
Let’s revisit our example with the JavaScript field declaration syntax:
class Image {
height = 0;
width;
constructor(height, width) {
this.height = height;
this.width = width;
}
}
The difference is our fields have been declared up-front. So our class definitions become more self-documenting, and the fields are always present.
Note: the fields can be declared with or without a default value!
Private Field Declarations
When we use private fields, the definition can be refined like so:
class Image {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
Private fields (declared with a #
) cannot be referenced outside of the class, only within the class body. This ensures that your classes’ users can’t depend on internals, which may change with version changes.
Note: Private fields cannot be created later through an assignment. They can only be declared up-front in a field declaration.
Child Classes Using 'extends'
We can use the extends
keyword with either class declarations or class expressions to create a class as a child of another class.
class Vehicle{
constructor(name) {
this.name = name;
}
sound() {
console.log(`${this.name} makes a sound.`);
}
}
class Car extends Vehicle{
constructor(name) {
super(name); // call the super class constructor and pass in the name parameter
}
sound() {
console.log(`The ${this.name} tooted its horn!`);
}
}
let c = new Car('Volkswagen');
c.sound(); // The Volkswagen tooted its horn!
If there is a constructor present in the subclass, it needs to first call super() before using “this”.
Function-based “classes” may also be extended:
function Vehicle (name) {
this.name = name;
}
Vehicle.prototype.sound = function () {
console.log(`${this.name} makes a sound.`);
}
class Car extends Vehicle{
speak() {
console.log(`The ${this.name} tooted its horn!`);
}
}
let c = new Car('Volkswagen');
c.sound(); // The Volkswagen tooted its horn!
Note: classes cannot extend regular objects! If you want to inherit from an object, use Object.setPrototypeOf()
:
const Vehicle = {
sound() {
console.log(`${this.name} makes a sound.`);
}
};
class Car{
constructor(name) {
this.name = name;
}
}
let c = new Car('Volkswagen');
c.sound(); // Volkswagen makes a sound.
Species
If you want to return Array
objects from an array class MyArray
. You can do so with the “species” pattern, which lets you override the default constructors.
If using methods such as map()
it’ll return the default constructor. Then you’ll want these methods to return a parent Array
object, instead of the MyArray
object. Symbol.specieslets
you do this, like so:
class MyArray extends Array {
// Overwrite species to the parent Array constructor
static get [Symbol.species]() { return Array; }
}
let a = new MyArray(1,2,3);
let mapped = a.map(x => x * x);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
The ‘Super’ Keyword
The super
keyword is used to call corresponding methods of the superclass. This is one advantage over a prototype-based inheritance. Let’s see an example:
class Volkswagen {
constructor(name) {
this.name = name;
}
sound() {
console.log(`${this.name} makes a sound.`);
}
}
class Beetle extends Volkswagen {
sound() {
super.sound();
console.log(`${this.name} toots it's horn.`);
}
}
let b = new Beetle('Herbie');
b.sound();
// Herbie makes a sound.
// Herbie toots it's horn.
Mix-ins
Mix-ins are templates for classes. An ECMAScript class can only have a single superclass, so multiple inheritances from tooling classes, for example, isn’t possible. The functionality must be provided by the superclass.
A function with a superclass as input and a subclass extending that superclass as output can be used to implement mix-ins in ECMAScript:
let calculatorMixin = Base => class extends Base {
calc() { }
};
let randomizerMixin = Base => class extends Base {
randomize() { }
};
A class that uses these mix-ins can then be written like this:
class First { }
class Second extends calculatorMixin(randomizerMixin(First)) { ...
We’ve taken a deep dive into JavaScript classes including class declarations, class expressions, constructors, instance properties, field declarations, extends, species, super, and mix-ins.
Conclusion
If you liked this blog post, follow me on Twitter where I post daily about Tech related things! If you enjoyed this article & would like to leave a tip — click here