This tutorial is out of date and no longer maintained.
In the early history of the JavaScript language, a cloud of animosity formed over the lack of a proper syntax for defining classes like in most object-oriented languages. It wasn’t until the ES6
spec release in 2015 that the class
keyword was introduced; it is described as syntactical sugar over the existing prototype-based inheritance.
At its most basic level, the class
keyword in ES6
is equivalent to a constructor function definition that conforms to prototype-based inheritance. It may seem redundant that a new keyword was introduced to wrap an already existing feature but it leads to readable code and lays the foundation upon which future object-oriented features can be built.
Before ES6
, if we had to create a blueprint (class) for creating many objects of the same type, we’d use a constructor function like this:
function Animal(name, fierce) {
Object.defineProperty(this, 'name', {
get: function() { return name; }
});
Object.defineProperty(this, 'fierce', {
get: function() { return fierce; }
});
}
Animal.prototype.toString = function() {
return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
}
This is a simple object constructor that represents a blueprint for creating instances of the Animal
class. We have defined two read-only own
properties and a custom toString
method on the constructor function. We can now create an Animal
instance with the new
keyword:
var Lion = new Animal('Lion', true);
console.log(Lion.toString()); // "A fierce Lion"
Great! It works as expected. We can rewrite the code using the ES6
class for a concise version:
class Animal {
constructor(name, fierce) {
this._name = name;
this._fierce = fierce;
}
get name() {
return this._name;
}
get fierce() {
return `This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
}
toString() {
return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
}
}
Let’s create an instance of the Animal
class with the new keyword as we did before:
let Lion = new Animal('Lion', true);
console.log(Lion.fierce);
console.log(Lion.toString())
Defining classes in ES6
is very straightforward and feels more natural in an object-oriented sense than the previous simulation using object constructors. Let’s take an in-depth look at the ES6
class by exploring some of its attributes and ramifications.
Making a transition from using the older object constructors to the newer ES6
classes shouldn’t be difficult at all since the class
keyword is just a special function
and exhibits expected function behavior. For example, just like a function, a class
can be defined by either a declaration or an expression, where the latter can be named or unnamed.
A class declaration is defined with the class
keyword and followed by the name of the class.
class Animal {}
We already used the class
declaration when we wrote the ES6
version of the Animal
constructor function :
class Animal {
constructor(name, fierce) {
this._name = name;
this._fierce = fierce;
}
}
A class expression allows for a bit more flexibility; a class may be named or unnamed, however, when a class expression is named, the name attribute becomes a local property on the class’ body and can be accessed using the .name
property.
An unnamed class expression skips the name after the class
keyword:
// unnamed
let animal = class {}
A named class expression, on the other hand, includes the name:
// named
let animal = class Animal {}
When comparing the object constructor to the
ES6 class
, it is worthy of note that, unlike the object constructor that can be accessed before its scope is defined because of hoisting, theclass
can’t and isn’t hoisted.While this may seem like a major limitation on
ES6
classes, it doesn’t have to be; goodES6
practice demands that if any function must mutate an instance of a class then it can be defined anywhere in the program but should be invoked only after theclass
itself has been defined.
After defining a class using any of the two stated methods, the curly brackets {}
should hold class members, such as instance variables, methods, or constructor; the code within the curly brackets make up the body of the class.
A class’ constructor is simply a method whose purpose is to initialize an instance of that class. This means that whenever an instance of a class is created, the constructor (where it is defined) of the class is invoked to do something on that instance; it could maybe initialize the object’s properties with received parameters or default values when the former isn’t available.
There can only be a single constructor method associated with a class so be careful not to define multiple constructor methods as this would result in a SyntaxError. The super keyword can be used within an object’s constructor to call the constructor of its superclass.
class Animal {
constructor(name, fierce) { // there can only be one constructor method
this._name = name;
this._fierce = fierce;
}
}
The code within the body of a class is executed in strict mode.
The body of a class usually comprises instance variables to define the state of an instance of the class, and prototype methods to define the behavior of an instance of that class. Before ES6
, if we needed to define a method on a constructor function, we could do it like this:
function Animal(name, fierce) {
Object.defineProperty(this, 'name', {
get: function() { return name; }
});
Object.defineProperty(this, 'fierce', {
get: function() { return fierce; }
});
}
Animal.prototype.toString = function() {
return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
}
Or
function Animal(name, fierce) {
Object.defineProperty(this, 'name', {
get: function() { return name; }
});
Object.defineProperty(this, 'fierce', {
get: function() { return fierce; }
});
this.toString = function() {
return 'A' + ' ' + (this.fierce ? 'fierce' : 'tame') + ' ' + this.name;
}
}
The two different methods we defined above are referred to as prototype methods and can be invoked by an instance of a class. In ES6, we can define two types of methods: prototype and static methods. Defining a prototype method in ES6 is quite similar to what we have above, except that the syntax is cleaner (we don’t include the prototype property) and more readable:
class Animal {
constructor(name, fierce) {
this._name = name;
this._fierce = fierce;
}
get name() {
return this._name;
}
get fierce() {
return ` This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
}
toString() {
return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
}
}
Here we first define two getter
methods using a shorter syntax, then we create a toString
method that basically checks to see if an instance of the Animal
class is a fierce or tame animal. These methods can be invoked by any instance of the Animal
class but not by the class itself.
ES6
prototype methods can be inherited by children classes to simulate an object-oriented behavior in JavaScript but under the hood, the inheritance feature is simply a function of the existing prototype chain and we’d look into this very soon.
All
ES6
methods cannot work as constructors and will throw a TypeError if invoked with the new keyword.
Static methods resemble prototype methods in the fact that they define the behavior of the invoking object but differ from their prototype counterparts as they cannot be invoked by an instance of a class. A static method can only be invoked by a class; an attempt to invoke a static method with an instance of a class would result in unexpected behavior.
A static method must be defined with the static keyword. In most cases, static methods are used as utility functions on classes.
Let’s define a static utility method on the Animal
class that simply returns a list of animals:
class Animal {
constructor(name, fierce){
this._name = name;
this._fierce = fierce;
}
static animalExamples() {
return `Some examples of animals are Lion, Elephant, Sheep, Rhinoceros, etc.`
}
}
Now, we can call the animalExamples()
method on the class itself:
console.log(Animal.animalExamples()); // "Some examples of animals are Lion, Elephant, Sheep, Rhinoceros, etc."
In object-oriented programming, it’s good practice to create a base class that holds some generic methods and attributes, then create other more specific classes that inherit these generic methods from the base class, and so on. In ES5
we relied on the prototype chain to simulate this behavior and the syntax would sometimes become messy.
ES6
introduced the somewhat familiar extends
keyword that makes inheritance very easy. A subclass can easily inherit attributes from a base class like this:
class Animal {
constructor(name, fierce) {
this._name = name;
this._fierce = fierce;
}
get name() {
return this._name;
}
get fierce() {
return `This animal is ${ this._fierce ? 'fierce' : 'tame' }`;
}
toString() {
return `This is a ${ this._fierce ? 'fierce' : 'tame' } ${this._name}`;
}
}
class Felidae extends Animal {
constructor(name, fierce, family) {
super(name, fierce);
this._family = family;
}
family() {
return `A ${this._name} is an animal of the ${this._family} subfamily under the ${Felidae.name} family`;
}
}
We have created a subclass here — Felidae (colloquially referred to as “cats”) — and it inherits the methods on the Animal
class. We make use of the super
keyword within the constructor method of the Felidae
class to invoke the super class’ (base class) constructor. Awesome, let’s try creating an instance of the Felidae
class and invoking and own method and an inherited method:
var Tiger = new Felidae('Tiger', true, 'Pantherinae');
console.log(Tiger.toString()); // "This is a fierce Tiger"
console.log(Tiger.family()); // "A Tiger is an animal of the Pantherinae subfamily under the Felidae family"
If a constructor is present within a sub-class, it needs to invoke
super()
before using “this”.It is also possible to use the
extends
keyword to extend a function-based “class”, but an attempt to extend a class solely created from object literals will result in an error.
At the beginning of this article, we saw that most of the new keywords in ES6
are merely syntactical sugar over the existing prototype-based inheritance. Let’s now take a look under the sheets and see how the prototype chain works.
While it’s nice to define classes and perform inheritance with the new ES6
keywords, it’s even nicer to understand how things work at the canonical level. Let’s take a look at JavaScript objects: All JavaScript objects have a private property that points to a second object (except in a few rare cases where it points to null
) associated with them, this second object is called the prototype
.
The first object inherits properties from the prototype
object and the prototype may, in turn, inherit some properties from its own prototype and it goes on like that until the last prototype on the chain has its prototype property equal to null
.
All JavaScript objects created by assigning an identifier the value of object literals share the same prototype object. This means that their private prototype property points to the same object in the prototype chain and hence inherits its properties. This object can be referred to in JavaScript code as Object.prototype
.
Objects created by invoking a class’ constructor or constructor function initialize their prototype from the prototype property of the constructor function. In other words, when a new object is created by invoking new Object()
, that object’s prototype becomes Object.prototype
just like any object created from object literals. Similarly, a new Date()
object will inherit from Date.prototype()
and a new Number()
from Number.prototype()
.
Nearly all objects in JavaScript are instances of
Object
which sits on the top of a prototype chain.
We have seen that it’s normal for JavaScript objects to inherit properties from another object (prototype) but the Object.prototype
exhibits a rare behavior where it does not have any prototype and does not inherit any properties (it sits on the top of a prototype chain) from another object.
Nearly all of JavaScript’s built-in constructors inherit from Object.prototype
, so we can say that Number.prototype
inherits properties from Object.prototype
. The effect of this relationship : creating an instance of Number
in JavaScript (using new Number()
) will inherit properties from both Number.prototype
and Object.prototype
and that is the prototype chain.
JavaScript objects can be thought of as containers since they hold the properties defined on them and these properties are referred to as " own properties" but they are not limited to just their own properties. The prototype chain plays a big role when a property is being sought on an object:
Let’s write some code to clearly simulate the behavior of prototypal inheritance in JavaScript.
We will be using the ES5
method — Object.create()
— for this example so let’s define it:
Object.create()
is a method that creates a new object, using its first argument as the prototype of that object.
let Animals = {}; // Animal inherits object methods from Object.prototype.
Animals.eat = true; // Animal has an own property - eat (all Animals eat).
let Cat = Object.create(Animals); // Cat inherits properties from Animal and Object.prototype.
Cat.sound = true; // Cat has its own property - sound (the animals under the cat family make sounds).
let Lion = Object.create(Cat); // Lion (a prestigious cat) inherits properties from Cat, Animal, and Object.prototype.
Lion.roar = true; // Lion has its own property - roar (Lions can roar)
console.log(Lion.roar); // true - This is an "own property".
console.log(Lion.sound); // true - Lion inherits sound from the Cat object.
console.log(Lion.eat); // true - Lion inherits eat from the Animal object.
console.log(Lion.toString()); // "[object Object]" - Lion inherits toString method from Object.prototype.
Here’s a verbal interpretation of what we did above:
Animal
object and it inherits properties from Object.prototype
Animal``'``s
own property — eat — to true (all Animals eat)Animal
(Therefore Cat inherits properties from Animal
and Object.prototype
)Cat's
own property — sound — to true (the animals under the cat family make sounds)Cat
(Therefore Lion inherits properties from Cat
, Animal
and Object.prototype
)Lion's
own property — roar — to true (Lions can roar)Lion
object and they all returned the right values by first seeking for the properties on the Lion
object then moving on to the prototypes (and prototypes of prototypes) where it wasn’t available on the former.This is a basic but accurate simulation of the prototypal inheritance in JavaScript using the prototype chain.
In this article, we have gone through the basics of ES6 classes and prototypal inheritance. Hopefully, you have learned a thing or two from reading the article. If you have any questions, leave them below in the comments section.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!