Classes in JavaScript - Explained

This article is an extended adaptation from Understanding Prototypes in JavaScript. This article is very in-depth, and may require a second reading to really grok it.

JavaScript is unique because it can be used as an object-orientated programming (OOP) language, but it also has a lot of functional aspects built into it. Furthermore, unlike classic OOP languages such as Java, its inheritance model is not class-based, but rather prototype-based.

So what's the difference?

Class-based inheritance

In class-based inheritance, everything object must belong to a class.

An object of the Car class must have all the methods and properties defined in the class, and nothing else. To add a new property or method to an object, you must create a new class and then a new instance of that class.

For inheritance, you have a parent class and child classes. The child classes will inherit all the parent's properties and methods.

Here's what a simple Java program would look like:

// Vehicle.java
public class Vehicle {
  // Constructor
  public Vehicle(int horsePower) {
    this.horsePower = horsePower;
  }
  public int horsePower;
  public int mileage = 1000;
}

// Car.java
public class Car extends Vehicle {
  public Car(int horsePower) {
    super(horsePower); // Calls Vehicle's constructor
    speed = 0;
  }
  public int speed;
}

// Main.java
public class Main {
  public static void main(String args[]) {
    Car myCar = new Car(500);
    System.console().writer().println(myCar.horsePower); // 500
    System.console().writer().println(myCar.mileage); // 1000
    System.console().writer().println(myCar.speed); // 0
  }
}

As you can see, we must give each variable a class - myCar will have the methods and properties of the Car class, and its parents', but nothing else.

Prototypical Inheritance

With JavaScript, which uses a prototype-based inheritance model, the properties and methods an object can take are not restricted. When you create a new object from the class, you can alter the object in whatever way you want afterwards - remove, change as well as add new properties. All the class did was to act as a blueprint from which new objects are stamped out - the object has no obligations to keep any properties.

In a prototype-based system, there are no classes. All objects are created by adding properties and methods to an empty object or by cloning an existing one. ~ Javascript object prototype by Helen Emerson

In other words, prototype 'classes' are simply objects themselves, and when we create new objects from these 'classes', we are copying certain properties of the 'class' to the new object. We will explain exactly which properties are copied in the rest of this article.

So even though ES6 introduced the class syntax, classes in JavaScript are different to classes in other OOP languages.

Function Constructors

What you traditionally thought of as 'JavaScript classes' are actually function constructors - functions which provide that blueprint, or functions which construct the object.

A function constructor is nothing special; any function can be a function constructor - you just have to call it with the new keyword.

const Foo = function () {};
const bar = new Foo();
bar; // {}
bar instanceof Foo; // true
bar instanceof Object; // true

This empty function does nothing in particular, so an empty object is constructed. We can add some logic to the function's body to specify what should be in the constructed object.

const Foo = function () {
  this.bar = "baz";
};
const qux = new Foo();
qux; // { bar: "baz" }
qux.bar; // "baz"

When called with the new keyword, this inside the function body refers to the object being constructed. So this.bar = "baz" assigns a new property bar on the new object and give it the value of "baz".

Function Constructors allow you to create new many objects of a similar structure.

Furthermore, the reference to the function constructor itself is added to the constructor property of the new object.

qux.constructor === Foo; // true

__proto__ and Prototypes

Objects created using function constructors do not only get the variables defined in the function body added to it, it also retains information about the function constructor that created it. It does this through the __proto__ property, common to all objects, and the prototype property, which is present only on functions.

Let's break down what we just said.

Every object has a __proto__ property.

const foo = {};
foo.__proto__; // Object
const bar = [];
bar.__proto__; // Object

If we look more closely at the object that these __proto__ properties store, we can see that they contain methods that we'd expect to find on that data type.

For example, our variable bar is an array, and in bar.__proto__ we find methods like push, pop, join etc.

So these are pre-defined methods that are accessible to the object created. So where do these methods come from?

These methods are stored in the prototype property of the object's function constructor.

Every function has a prototype object.

const Foo = function () {};
Foo.prototype; // Foo

Since function constructors are also just functions, function constructors also have the prototype object.

From our other post - (Not) Everything in JavaScript is an Object - we know that every function is a type of object with some special methods and properties; one of these properties is prototype.

In our bar array example, the function constructor is Array.

bar.constructor; // function Array() { ... }
bar.constructor === Array; true

And when the new object is created, the prototype of the built-in Array function constructor is copied into the __proto__ of the new object.

foo.__proto__ === Object.prototype; // true
bar.__proto__ === Array.prototype; // true
const Baz = function () {};
const qux = new Baz(); // {}
qux.__proto__ === Baz.prototype; // true

The new object has access to the properties added to it in the function constructor's body, as well as properties defined in the function's prototype object.

const Foo = function () {
    this.bar = "abc";
}
Foo.prototype.baz = "def";
const qux = new Foo();
qux.bar; // "abc"
qux.baz; // "def"

So, when should you define object properties/method as a property of the function constructor's prototype object, and when should you define it inside the function constructor's body?

Prototype Inheritance Chain

To answer that, we need to understand the prototype inheritance chain. Let's look at another example:

const Foo = function () {
    this.bar = "abc"
}
Foo.prototype.bar = "def"
const qux = new Foo();
qux.bar; // "abc"
qux.__proto__.bar; // "def"

As you can see above, the bar property assigned inside the function's body takes precedence over the one set in the Foo.prototype.

From this, we can see that when trying to access an object property or method, it will first look at the object's own property first, and if it cannot find it, only then does it look at the object's __proto__ property (which is copied from the object's function constructor's prototype property).

Let's do one last experiment:

qux.toString(); // "[object Object]"

How were we able to access the toString method when we have neither defined it in the function constructor's body, nor in the prototype object?

This is because JavaScript will continue to look up the prototype chain until it finds a method called toString. This is the order of events that's happening behind the scene.

  1. Try to find the toString method in the object's (qux) own properties
  2. Unable to find it
  3. Go to the object's function constructor's (Foo) prototype object and try to find the toString method there
  4. Unable to find it
  5. Go to Foo's function constructor's (Object) prototype object and try to find the toString method there
  6. Finds it, and uses that method

We can confirm this by running:

qux.toString === Object.prototype.toString; // true

And you can see the actual inheritance chain by checking the qux object's, and its parents' __proto__ property.

The prototype inheritance chain is the idea that JavaScript will look for a requested property or method sequentially, each time going higher up the prototype chain, until it finds the property / method requested.

So, going back to our original question - where should a method be defined - on the object itself? Or on the function constructor's prototype object?

const Foo = function () {
    this.bar = "abc"
}
Foo.prototype.bar = function () { console.log(this); }; // `this` refers to the object instance

When adding methods to the Foo.prototype, all child objects can access this method, and the method is only constructed once. The bar property is constructed once for each instance of the object. So whenever possible, it's better to define methods in the function constructor's prototype object, and define properties specific to the object itself inside the function constructor's body.

ECMAScript Classes

ECMAScript 2015 provided syntactical sugar for these function constructors using the class keyword. It does not actually change the way inheritance works in JavaScript, but simply make the syntax clearer and less bloated.

The class keyword is ECMAScript 2015 syntactical sugar, the underlying mechanism of prototype-based inheritance using function constructors remains unchanged.

You can define a class using the class keyword.

class Foo {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  bar() {}
  static baz() {}
}

We are using the class declaration syntax; you can also use the class expression syntax.

Inside the class body, you can define methods and properties.

Methods

There are three types of methods in a JavaScript class:

  • Constructor
  • Prototype Methods
  • Static Methods
Constructor

The constructor is a special method that is called each time an instance of the class is created; most people use the constructor to set initial values for the instance's properties.

There must always be one, and only one, constructor inside a class. If a constructor is not specified, a default constructor would be used:

// Default constructor for non-derived classes
constructor() {}

// Default constructor for classes extended from a parent class
// (See 'extending a class' below)
constructor(...args) {
  super(...args);
}
Prototype Methods vs Static Methods

Prototype methods are available inside the instance and can access the instance's properties. Prototype methods are only callable on the instance, and not on the class itself.

Static methods, on the other hand, are independent on the instance of the class, and is callable on the class itself. In fact, static methods cannot be called on the instances of the class.

class Foo {
  static bar() { console.log(this) } // Static method
  baz() { console.log(this) } // Prototype method
}
Foo.bar(); // class Foo { ... }, the class Foo itself
Foo.baz(); // Uncaught TypeError: Foo.baz is not a function

const qux = new Foo();
qux.bar() // Uncaught TypeError: baz.bar is not a function
qux.baz() // Foo {}, an instance of Foo

Prototype methods are callable on the instances and not on the class, static methods are callable on the class itself but not on its instance(s)

Relationship to Function Constructors

Since class is just syntactic sugar, the same features can be achieved without the class keyword, using function constructors.

// ES6
class Foo {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  bar() { console.log("bar"); }
  static baz() { console.log("baz"); }
}

// ES5
function Foo(x, y) {
  this.x = x;
  this.y = y;
}
Foo.prototype.bar = function () { console.log(this); }
Foo.baz = function () { console.log("baz"); }

The ES6 class's constructor is actually the function constructor's function body; prototype methods are methods on the function constructor's prototype object, and has access to the this property (which refers to the object instance); static methods become methods on the function constructor itself (and do not have access to the object instance).

extending a class

The extend keyword allows you to create a child class from a parent class. It's also known as subclassing.

When you extend from a parent class, the super keyword becomes available. You can think of super as a construct that represents its parent class.

class Vehicle {
  constructor() {
    this.name = "Vehicle";
  }
  move() { console.log("moving") }
  static name() { console.log(this.name) }
}
class Car extends Vehicle {
  constructor() {
    super(); // Calls the constructor of Vehicle
    this.name = "Car";
  }
  move() {
    super.move();
    console.log("Car.move");
  }
}

For example, calling super() inside Car's constructor is the equivalence of calling Vehicle's constructor. If the parent's constructor accepts arguments in its constructor, you can pass arguments into the super call too.

Calling super.move() is the equivalence of calling Vehicle.prototype.move().

A class must first extend from a parent class before it can use the super keyword.

class Foo {
  constructor() {
    super(); // Uncaught SyntaxError: 'super' keyword unexpected here
  }
}
Relationship to Function Constructors

To finish, let's see what extends desugars into:

# ES6
class Vehicle {
  constructor() {
    this.name = "Vehicle";
  }
  move() { console.log("moving") }
  static name() { console.log(this.name) }
}
class Car extends Vehicle {
  constructor() {
    super(); // Calls the constructor of Vehicle
    this.name = "Car";
  }
  move() {
    super.move();
    console.log("Car.move");
  }
}

# ES5
function Vehicle () {
  this.name = "Vehicle";
}
Vehicle.prototype.move = function () { console.log("moving") }
Vehicle.name = function () { console.log(this.name) }

function Car () {
  Vehicle.call(this);
  this.name = "Car";
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.move = function move() {
  Vehicle.prototype.move();
  console.log("Car.move");
}
Car.prototype.constructor = Vehicle;

Summary

  • JavaScript do not have classes in the classical sense
  • Inheritance is achieved through a prototype-based model, not a class-based one
  • Function constructors encapsulates the logic to create new objects based on a blueprint
  • Any function can be a function constructor
  • A function acts as a function constructor by using the keyword new
  • Every function has a prototype property
  • Every object (a function is an object) has a __proto__ property
  • When a new object is created from a function constructor, the function constructor's prototype object is copied into the object instance's __proto__ property.
  • When JavaScript tries to look for a property or method on the object, it will follow the prototype inheritance chain
  • ES6 classes desugars into function constructors
  • Use static methods to provide utility functions, use prototype methods to that acts on the object instances themselves, and use the constructor to give initial values to each individual object, based on the values passed into the constructor.

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com