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.