Classes in JavaScript: An Introduction

Abstract image used to represent Classes in JavaScript: An Introduction
Image by Ashkan Forouzani.

Since ES6, JavaScript classes have given object creation a familiar shape: a constructor, instance methods, inheritance, and a clear name for the thing being modelled. They are still built on the prototype system, though, and choosing class syntax does not automatically make a design more modular or easier to maintain.


Basics of Classes in JavaScript

Classes in JavaScript provide a blueprint for creating objects with properties and methods. They are defined using the class keyword, followed by the class name, and an optional constructor method.

For example:

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.running = false;
  }

  start() {
    this.running = true;
    console.log(`Starting ${this.make} ${this.model} ${this.year}`);
  }

  stop() {
    this.running = false;
    console.log(`Stopping ${this.make} ${this.model} ${this.year}`);
  }
}

Here, we've defined a Car class with a constructor which accepts three parameters: make, model, and year. We've also defined two methods: start and stop, which set the running property and log a message to the console.

To use this, we create a new instance of the Car class using the new keyword like this:

const myCar = new Car('Volkswagen', 'Golf', 2016);

This creates a new instance of the Car class called myCar with the specified make, model, and year, which can be accessed like so:

console.log(myCar.make);  //=> 'Volkswagen'
console.log(myCar.model);  //=> 'Golf'

More Advanced Class Use

So far we've only looked at fairly rudimentary use for class. Instead of a Volkswagen, let's embrace the electriccar future. In JavaScript, classes can also inherit from other classes using the extends keyword. For example:

class ElectricCar extends Car {
  constructor(make, model, year, batteryCharge) {
    super(make, model, year);
    this.batteryCharge = batteryCharge;
  }

  start() {
    super.start();
    console.log(`The battery is charged to ${this.batteryCharge}%`);
  }
}

Here, we've defined a new class called ElectricCar, which extends the Car class, and we've added a new property called batteryCharge. Finally, we've overridden the start method to include a message about the battery charge (which you would presumedly receive whilst starting your car remotely).

To create a new instance of the ElectricCar class, we can use the same syntax as before, although with an additional argument:

const myElectricCar = new ElectricCar('Tesla', 'Model S', 2016, 95);

With this new electric car created, we can start it up remotely by calling the start method, which logs the starting message followed by the current charge level:

myElectricCar.start();
//=> 'Starting Tesla Model S 2016'
//=> 'The battery is charged to 95%'

Potential Issues

ES2015 classes still use JavaScript's prototypebased inheritance. The syntax gives us a convenient place to define the constructor and shared methods, but it doesn't remove the need to understand that prototype chain. It also has rules of its own: class bodies run in strict mode, and a class constructor must be called with new.

The ES2015 class syntax discussed here has no dedicated syntax for private members or methods. Closures can keep values out of reach of other code, though that is a different pattern from declaring a private member on the class.


Alternative Strategies

Obviously, classes are relatively new, and developers have still had alternative strategies and patterns to implement objectoriented programming (OOP) concepts. For completeness, here are some of the common strategies used. Rather than using the carbased examples I've used above, I'm switching to a more triedandtested example of using people, which I feel is a little easier when offering multiple examples for the same (ish) outcome. Hopefully, the juxtaposition isn't too jarring.

Function Constructors

A constructor function is an ordinary function intended to be called with new. In this example, new creates an object linked to Person.prototype and calls Person with that object as this. The constructor property is normally inherited from Person.prototype; new does not add a separate constructor property to the instance. For example:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    console.log(
      `Hello, my name is ${this.name} and I am ${this.age} years old.`
    );
  };
}
let adam = new Person('Adam', 25);
adam.greet();  //=> "Hello, my name is Adam and I am 25 years old."

Prototypes

An object can inherit properties and methods through its prototype. That link points to another object, or to null at the end of the chain. Prototypes allow developers to share code between objects and avoid duplicating code. For example:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function () {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

let max = new Person('Max', 25);
max.greet();  //=> "Hello, my name is Max and I am 25 years old."

Object Literals

An object literal is a way to define an object using curly braces. Object literals are a simple way to create objects with properties and methods:

let owen = {
  name: 'Owen',
  age: 25,
  greet: function () {
    console.log(
      `Hello, my name is ${this.name} and I am ${this.age} years old.`
    );
  },
};

owen.greet();  //=> "Hello, my name is Owen and I am 25 years old."

Factory Functions

A factory function is a function that returns an object. Factory functions can be used to create multiple instances of an object with different values. Here's our Person example again:

function createPerson(name, age) {
  return {
    name: name,
    age: age,
    greet: function () {
      console.log(
        `Hello, my name is ${this.name} and I am ${this.age} years old.`
      );
    },
  };
}

let carl = createPerson('Carl', 25);
carl.greet();  //=> "Hello, my name is Carl and I am 25 years old."

These are all objectorientation strategies which were used before classes were introduced in JavaScript, and are still widely used today. The choice of which strategy to use depends on the specific needs of the project and personal preference, although some are certainly easier to read than others...


The Wrap‑up

Use a class when instances share behaviour and the model benefits from an explicit constructor and prototype methods. Use a factory or object literal when that shape is clearer. The useful choice is the one that makes state and behaviour easiest to understand in this project, not the one that looks most conventionally objectoriented.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.