JavaScript introduced the class keyword in 2015 with the release of ES6. React made classes an indispensable language feature when they introduced support for using extends React.Component instead of React.createClass() in 2015, and removed support for React.createClass() entirely in 2017 in favor of classes. Today, classes are a fundamental part of JavaScript, and many new JavaScript devs don't remember a time before classes. In this article, I'll provide an overview of how classes work in JavaScript: how to declare classes, what features JavaScript provides with classes, and how inheritance works.

Working With Classes

Here's how you define a basic class MyClass, and create an instance of MyClass.

class MyClass {
  constructor() {
    this.answer = 42;
  }
}

const obj = new MyClass();
obj.answer; // 42

You must instantiate a class with new. Calling MyClass() without new throws an error:

// TypeError: Class constructor MyClass cannot be invoked without 'new'
MyClass();

A class is technically a function, although the ECMAScript spec explicitly disallows calling a class without new. In fact, the typeof operator identifies MyClass as a function.

typeof MyClass; // 'function'
MyClass instanceof Function; //  true

To check whether an object is an instance of a class, you should use the instanceof operator. You can also check whether the constructor property is equal to MyClass.

obj instanceof MyClass; // true
// Works, but `instanceof` is better...
obj.constructor === MyClass;

// Because `instanceof` is immune to overwriting the `constructor` property
const obj = {};
obj.constructor = MyClass;
obj instanceof MyClass; // false
obj.constructor === MyClass; // true

Like functions, classes in JavaScript are variables like any other. You can assign a class to a variable, overwrite that class, and pass a class as a parameter to a function. Like functions, you can also declare classes with or without explicit names.

let Foo = class {
  constructor() {
    this.answer = 42;
  }
}

Foo = class {
  constructor() {
    this.answer = 43;
  }
}

console.log(Foo); // '[Function: Foo]'
console.log(new Foo()); // 'Foo { answer: 43 }'

Unlike functions, classes are never hoisted. In the below example the function Foo() prints successfully, because JavaScript looks ahead and 'hoists' Foo() to the top of the function. But trying to print the class Bar throws a reference error, because JavaScript does not hoist class definitions.

Statics, Methods, Getters, Setters

ES6 classes support numerous object-oriented programming constructs, like static functions, instance methods, and getters and setters.

Static functions are functions defined on the class itself. You call ClassName.staticName(), and, within the static function, this refers to the class.

class MyClass {
  static myStatic() {
    this; // [Function: MyClass]
    return 42;
  }
}

MyClass.myStatic(); // '42'

Instance methods are functions on instances of the class. When you create a new object using new MyClass(), you can call obj.myMethod(). Within myMethod(), this refers to obj.

class MyClass {
  constructor() {
    this.answer = 42;
  }

  myMethod() {
    this === obj; // true
    return this.answer;
  }
}

const obj = new MyClass();
obj.myMethod(); // 42

Getters and setters let you define functions that run when you access or assign a property on an instance of the class. For example, getters and setters can let you convert values to numbers when you set the property.

In the below example, instances of MyClass have a special property num that the class tries to convert to a number. The num property has a getter function that JavaScript executes when you access obj.num, and a setter function that JavaScript executes when you assign to obj.num using = or Object.assign(). The setter function converts num to a number, and throws an error if it could not convert the given value to a number.

class MyClass {
  get num() {
    return this._num;
  }

  set num(v) {
    const parsed = parseFloat(v);
    if (Number.isNaN(parsed)) {
      throw new Error(`"${v}" is not a number`);
    }
    this._num = parsed;
  }
}

const obj = new MyClass();
obj.num; // undefined;

obj.num = '42';
obj.num; // 42

// Error: "not a number" is not a number
obj.num = 'not a number';
// Error: "not a number" is not a number
Object.assign(obj, { num: 'not a number' });

Inheritance

Inheritance is one of the four core concepts of object-oriented programming. Besides syntactic sugar, the big advantage of using ES6 classes over pre-ES6 functions as class definitions is cleaner inheritance.

JavaScript class inheritance is still prototype-based under the hood, but extends abstracts away prototypes. Without having to write prototype, class inheritance in JavaScript looks a lot like inheritance in more "proper" object-oriented languages like Java. Here's a basic example of inheritance using ES6 classes:

class BaseClass {  
  static foo() { return 1; }
  static bar() { return 2; }

  a() { return 3; }
  b() { return 4; }
}

class ChildClass extends BaseClass {
  static bar() {
    // `super` is how you reference the parent class's statics and methods,
    // just like in Java
    return super.bar() * 2;
  }

  b() {
    return super.b() * 2;
  }
}

ChildClass.foo(); // 1
ChildClass.bar(); // 2 * 2 = 4

const obj = new ChildClass();
obj.a(); // 3
obj.b(); // 2 * 4 = 8

obj instanceof ChildClass; // true
obj instanceof BaseClass; // true

The class A extends B syntax means the child class A has the same members (including statics, methods, getters, and setters) as the base class B, but can also override B's members. Here's how you would do the same thing using pre-ES6 prototype-based inheritance.

function BaseClass() {}  

BaseClass.foo = () => 1;
BaseClass.bar = () => 2;

BaseClass.prototype = Object.create(Object.prototype);
BaseClass.prototype.a = () => 3;
BaseClass.prototype.b = () => 4;

function ChildClass() {}

Object.assign(ChildClass, BaseClass);
ChildClass.prototype = Object.create(BaseClass.prototype);

ChildClass.bar = () => BaseClass.bar() * 2;
ChildClass.prototype.b = function() {
  return BaseClass.prototype.b.call(this) * 2;
};

Like in Java, you call the parent class's constructor using super, and you need to call super() in the constructor before accessing this.

class BaseClass {
  constructor() { this.answer = 42; }
}

class Child1 extends BaseClass {
  constructor() {
    super();
    ++this.answer;
  }
}

class Child2 extends BaseClass {
  constructor() {
    this.answer = 43;
    super();
  }
}


new Child1(); // works

// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
new Child2();

Differences vs Node.js util.inherits()

Node.js has a native inherits() function that many developers used for inheritance before ES6. The key difference between inherits() and extends is that Node.js inherits() does not inherit statics.

const util = require('util');

class BaseClass {
  static foo() { return 1; }
  bar() { return 2; }
}

class Child1 extends BaseClass {}

function Child2() {}
util.inherits(Child2, BaseClass);

new Child1().bar(); // 2
new Child2().bar(); // 2

Child1.foo(); // 1
// The `Child2` pseudo-class does **not** inherit the `foo()` static function
//  from `BaseClass`
Child2.foo(); // TypeError: Child2.foo is not a function

Moving On

Classes are a fundamental part of JavaScript, and ES6 classes give you syntax and inheritance that closely mimic those of OOP languages like Java. JavaScript still uses prototype-based inheritance under the hood, which comes with several corner cases, but extends behaves as you would expect with the exception of static properties. On the bright side, JavaScript classes are just variables, which means you can assign classes to variables and pass classes as parameters to functions without any special syntax.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus