In-Depth Understanding of the Underlying Principles of Classes in ES6

ES6 introduced the class feature, making JavaScript’s object-oriented programming style more concise and easier to understand. Although classes resemble the classical object-oriented model of other programming languages, their underlying mechanism is still JavaScript’s existing prototype-based inheritance. This article delves into the underlying principles of ES6 classes to help you better understand how they work.

1. The Essence of Classes is Constructors

A class defined in ES6 is essentially a constructor. You can verify the nature of a class using the typeof keyword:

1
class Person {
2
constructor(name) {
3
this.name = name;
4
}
5
}
6
7
console.log(typeof Person); // "function"

The class definition is ultimately compiled into a function, meaning that a class is just syntactic sugar for a constructor.

2. How Constructors Work

The constructor method in a class is used to initialize class instances. When creating an instance of a class, the new keyword calls this constructor:

1
class Person {
2
constructor(name) {
3
this.name = name;
4
}
5
}
6
7
const p = new Person("Alice");
8
console.log(p.name); // "Alice"

This is very similar to traditional ES5 function constructors:

1
function Person(name) {
2
this.name = name;
3
}
4
5
const p = new Person("Alice");
6
console.log(p.name); // "Alice"

3. Prototype Inheritance

The methods within a class are defined on its prototype, which is entirely consistent with the ES5 prototype inheritance mechanism. Whenever a method is defined in a class, it is actually added to the class’s prototype:

1
class Person {
2
constructor(name) {
3
this.name = name;
4
}
5
6
greet() {
7
console.log(`Hello, ${this.name}!`);
8
}
9
}
10
11
const p = new Person("Alice");
12
p.greet(); // "Hello, Alice!"

The same result can be achieved in ES5 as follows:

1
function Person(name) {
2
this.name = name;
3
}
4
5
Person.prototype.greet = function () {
6
console.log(`Hello, ${this.name}!`);
7
};
8
9
const p = new Person("Alice");
10
p.greet(); // "Hello, Alice!"

The underlying inheritance mechanism of classes is still implemented through the prototype chain.

4. Class Inheritance Mechanism

ES6 provides the extends keyword to support class inheritance, which relies on JavaScript’s existing prototype-based inheritance. When a subclass inherits from a parent class, the subclass’s prototype points to the parent class’s prototype, thus achieving method inheritance:

1
class Animal {
2
constructor(name) {
3
this.name = name;
4
}
5
6
speak() {
7
console.log(`${this.name} makes a noise.`);
8
}
9
}
10
11
class Dog extends Animal {
12
speak() {
13
console.log(`${this.name} barks.`);
14
}
15
}
16
17
const d = new Dog("Rex");
18
d.speak(); // "Rex barks."

This is equivalent to manually implementing inheritance in ES5 using the prototype chain:

1
function Animal(name) {
2
this.name = name;
3
}
4
5
Animal.prototype.speak = function () {
6
console.log(`${this.name} makes a noise.`);
7
};
8
9
function Dog(name) {
10
Animal.call(this, name);
11
}
12
13
Dog.prototype = Object.create(Animal.prototype);
14
Dog.prototype.constructor = Dog;
15
16
Dog.prototype.speak = function () {
17
console.log(`${this.name} barks.`);
18
};
19
20
const d = new Dog("Rex");
21
d.speak(); // "Rex barks."

5. Using super to Call Parent Class Methods

The super keyword allows a subclass to call the parent class’s constructor and methods. In the subclass constructor, super is used to call the parent class’s constructor, inheriting the parent’s properties and methods:

1
class Animal {
2
constructor(name) {
3
this.name = name;
4
}
5
}
6
7
class Dog extends Animal {
8
constructor(name, breed) {
9
super(name); // Call the parent class's constructor
10
this.breed = breed;
11
}
12
}
13
14
const d = new Dog("Rex", "Labrador");
15
console.log(d.name); // "Rex"
16
console.log(d.breed); // "Labrador"

In ES5, a similar effect can be achieved by explicitly calling the parent class constructor:

1
function Animal(name) {
2
this.name = name;
3
}
4
5
function Dog(name, breed) {
6
Animal.call(this, name); // Call the parent class's constructor
7
this.breed = breed;
8
}
9
10
const d = new Dog("Rex", "Labrador");
11
console.log(d.name); // "Rex"
12
console.log(d.breed); // "Labrador"

Summary

The introduction of classes in ES6 provides a more concise syntax for object-oriented programming in JavaScript, but its underlying mechanics still rely on constructors and the prototype chain. Classes are essentially syntactic sugar for constructors, and class inheritance is based on the prototype chain. With super, subclasses can call the parent class’s constructor and methods, achieving inheritance. Understanding the underlying principles of classes helps us better utilize JavaScript’s object-oriented features in development.