ES6 Classes & Class Inheritance - extends & super
This comprehensive guide covers the fundamentals of ES6 Classes and Class Inheritance in JavaScript, including class declarations, methods, inheritance using the `extends` keyword, and the `super` keyword. Dive into creating, extending, and overriding classes with real-world examples and code snippets.
Introduction to ES6 Classes & Class Inheritance
JavaScript, the heart of the web, has undergone significant enhancements with the introduction of ECMAScript 6 (ES6) in 2015. One of the most notable features is the introduction of classes, which provide a more structured way to create objects and manage inheritance. If you're new to JavaScript or haven't delved into ES6, this guide is your perfect starting point. Let's explore the world of ES6 classes, inheritance, and the powerful extends
and super
keywords.
Understanding Classes
At the core, a class is a blueprint for creating objects. In traditional object-oriented programming languages like Java or C++, classes are essential for defining the structure and behavior of objects. ES6 introduced a more familiar and cleaner syntax for creating classes, making it easier for developers to adopt object-oriented programming principles in JavaScript.
What is a Class?
A class is essentially a template for creating objects. It encapsulates data for the object and methods to manipulate that data. Before ES6, you could create objects using constructor functions, but ES6 classes provide a more readable and maintainable approach.
Imagine a class as a blueprint for a car. Just like how a blueprint specifies the shape, size, and components of a car, a class specifies the properties and methods (functions) that an object can have.
Creating a Class
There are two main ways to define a class in JavaScript: using a class declaration or a class expression.
Class Declaration
To declare a class, you use the class
keyword followed by the name of the class. Here’s an example:
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021);
console.log(myCar.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
In this example, we define a Car
class with a constructor that initializes the properties make
, model
, and year
. The displayInfo
method is used to return a string with the car's details.
Class Expression
A class expression is similar to a class declaration, but it can be named or unnamed. Named class expressions can be referenced with their name within the class body, while unnamed class expressions can only be referenced by the variable they are assigned to.
const Vehicle = class {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
};
const myVehicle = new Vehicle('Bike');
console.log(myVehicle.startEngine()); // Output: Bike engine started.
In this example, an unnamed class expression is assigned to the variable Vehicle
. The class defines a startEngine
method that returns a string indicating that the engine has started.
ES5 vs ES6 Classes
Before ES6, you would create objects using constructor functions. Here’s how you might define a similar Car
class using ES5 syntax:
function CarES5(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
CarES5.prototype.displayInfo = function() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
};
const myCarES5 = new CarES5('Toyota', 'Corolla', 2021);
console.log(myCarES5.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
Comparing the ES5 and ES6 methods:
- ES5: You define constructor functions and then add methods to the prototype. This approach works but can be less intuitive and error-prone.
- ES6 Classes: The syntax is more concise and intuitive. You define the constructor and methods within the class body, making the code cleaner and easier to understand.
Defining Methods and Properties
Methods and properties are key components of a class. We’ll explore how to define and use them in ES6 classes.
Constructor Method
The constructor method is a special method for initializing new objects created with a class. It runs automatically when a new instance of the class is created using the new
keyword.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
In the above example, the constructor
method takes three parameters (make
, model
, and year
) and assigns them to the properties of the class instance.
Parameters in Constructor
You can pass parameters to the constructor to initialize the object with specific values. For instance, the Car
class above takes three parameters that specify the make, model, and year of the car.
Accessing Properties in Constructor
Inside the constructor, you can access the class properties using the this
keyword. This keyword refers to the instance of the class, allowing you to define and manipulate its properties.
Instance Methods
Instance methods are functions that can be called on an instance of a class. They operate on the instance properties and can perform operations using those properties.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
calculateAge() {
const year = new Date().getFullYear();
return `This car is ${year - this.year} years old.`;
}
}
In this example, the Car
class has two instance methods: displayInfo
and calculateAge
. The calculateAge
method calculates the age of the car based on its year of manufacture.
Static Methods
Static methods are methods that are bound to the class itself and not to instances of the class. They can be called directly on the class without creating an instance. Static methods are often used for utility functions that perform operations that don't require accessing the instance.
class MathUtils {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
console.log(MathUtils.add(5, 3)); // Output: 8
console.log(MathUtils.multiply(5, 3)); // Output: 15
In this example, MathUtils
is a class with two static methods: add
and multiply
. These methods can be called using the class name directly, without creating an instance of the class.
this
Keyword in ES6 Classes
Using The this
keyword is a fundamental concept in JavaScript, and its behavior can vary depending on the context. In the context of classes, this
refers to the instance of the class.
this
Scope of Inside class methods, this
refers to the instance of the class, allowing you to access the instance's properties and methods.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021);
console.log(myCar.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
In this example, the displayInfo
method uses this
to access the year
, make
, and model
properties of the class instance.
this
Arrow Functions and Arrow functions do not have their own this
context. Instead, they inherit the this
value from the enclosing execution context. This can be useful when you want to maintain the this
value within a nested function.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
displayInfoLater() {
setTimeout(() => {
console.log(`This car is a ${this.year} ${this.make} ${this.model}.`);
}, 1000);
}
}
const myCar = new Car('Toyota', 'Corolla', 2021);
myCar.displayInfoLater(); // Output after 1 second: This car is a 2021 Toyota Corolla.
In this example, the arrow function inside displayInfoLater
uses the this
value from the Car
class, ensuring that it refers to the correct instance.
Inheritance in JavaScript
Inheritance is a fundamental concept in object-oriented programming that allows you to create a new class (child class) based on an existing class (parent class). This promotes code reusability and a logical hierarchy.
What is Inheritance?
Inheritance allows a subclass to inherit properties and methods from a parent class. This means you can create a base class with common functionality and then extend it to create more specific subclasses. Inheritance helps in reducing code duplication and organizing code in a more structured manner.
extends
Keyword
Using The extends
keyword is used to create a subclass that inherits from a parent class. The subclass can add new methods and properties or override existing ones from the parent class.
class Vehicle {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
}
class Car extends Vehicle {
constructor(make, model, year) {
super(type, make, model, year);
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021);
console.log(myCar.startEngine()); // Output: Car engine started.
console.log(myCar.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
In this example, the Car
class extends the Vehicle
class. The Car
class inherits the startEngine
method from the Vehicle
class and adds its own displayInfo
method.
Example of Basic Inheritance
Let’s look at another example to understand inheritance better:
class Shape {
constructor(colors) {
this.colors = colors;
}
displayColors() {
return `Colors: ${this.colors.join(', ')}`;
}
}
class Circle extends Shape {
constructor(colors, radius) {
super(colors);
this.radius = radius;
}
area() {
return Math.PI * Math.pow(this.radius, 2);
}
}
const myCircle = new Circle(['red', 'blue'], 5);
console.log(myCircle.displayColors()); // Output: Colors: red, blue
console.log(myCircle.area()); // Output: 78.53981633974483
In this example, the Circle
class extends the Shape
class. The Circle
class inherits the displayColors
method from the Shape
class and adds its own area
method to calculate the area of the circle.
Accessing Parent Class Methods
In subclasses, you can access methods and properties from the parent class using the super
keyword.
class Vehicle {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
}
class Car extends Vehicle {
constructor(make, model, year) {
super(type);
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `${super.startEngine()} It is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021);
console.log(myCar.displayInfo()); // Output: Car engine started. It is a 2021 Toyota Corolla.
In this example, the displayInfo
method in the Car
class uses super.startEngine()
to call the startEngine
method from the Vehicle
class.
super
Keyword
The The super
keyword is used to call the constructor or methods from a parent class. It is crucial for inheriting and extending the functionality of the parent class.
super
to Call Parent Constructor
Using When extending a class, you must call the parent constructor using super()
in the subclass constructor. This is necessary to initialize the properties of the parent class.
class Vehicle {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
}
class Car extends Vehicle {
constructor(make, model, year, type) {
super(type);
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021, 'Car');
console.log(myCar.startEngine()); // Output: Car engine started.
console.log(myCar.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
In this example, the Car
class calls the Vehicle
class’s constructor using super(type)
, initializing the type
property of the Vehicle
class.
super
to Call Parent Methods
Using You can also use super
to call methods from the parent class. This is particularly useful when you want to extend or override a method from the parent class while still using its functionality.
class Vehicle {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
stopEngine() {
return `${this.type} engine stopped.`;
}
}
class Car extends Vehicle {
constructor(make, model, year, type) {
super(type);
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
startEngine() {
return `${super.startEngine()} It is ready to drive.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021, 'Car');
console.log(myCar.startEngine()); // Output: Car engine started. It is ready to drive.
In this example, the Car
class overrides the startEngine
method from the Vehicle
class. Inside the startEngine
method, super.startEngine()
is used to call the startEngine
method from the Vehicle
class and extend its functionality.
super
Example: Simple Inheritance with Here’s a more comprehensive example illustrating class inheritance with super
:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${super.speak()} Its breed is ${this.breed}.`;
}
}
const myDog = new Dog('Rex', 'Golden Retriever');
console.log(myDog.speak()); // Output: Rex makes a noise. Its breed is Golden Retriever.
In this example, the Dog
class extends the Animal
class. The Dog
class calls the Animal
class’s constructor using super(name)
and adds a breed
property. The speak
method in the Dog
class overrides the method from the Animal
class and extends its functionality by adding information about the breed.
class vs function Keyword
While ES6 classes and constructor functions serve similar purposes, they have some differences in syntax and behavior.
Comparison in Creation and Usage
- Classes: Classes are defined using the
class
keyword and have a constructor method to initialize the instance. They are block-scoped and not hoisted, meaning you cannot use them before declaring them in your code. - Functions: Functions can be defined using function declarations or expressions. They are hoisted, which means you can call a function before its declaration in the code. Functions can also be used to create objects using the
new
keyword, but they lack many of the features of classes, such as inheritance and easier method definitions.
// Function declaration
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.startEngine = function() {
return `${this.type} engine started.`;
};
// ES6 Class
class VehicleClass {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
}
In this example, both Vehicle
(constructed using a function) and VehicleClass
(constructed using a class) serve the same purpose but have different syntaxes. The class syntax is more concise and intuitive.
When to Use Classes vs Functions
- Classes: Use classes when you’re building large-scale applications using modern JavaScript and need the features of object-oriented programming, such as inheritance and static methods. Classes are more readable and maintainable.
- Functions: Use functions when you’re working in environments that do not support ES6 or when you need to define simple functions without the overhead of class syntax.
Private Properties and Methods
ES6 introduces private properties and methods, which are not accessible from outside the class. These features enhance encapsulation and protect the internal state of the class.
Private Instance Fields
Private instance fields are denoted by a #
symbol before the property name. They are accessible only within the class.
class Vehicle {
#engineType;
constructor(type, engineType) {
this.type = type;
this.#engineType = engineType;
}
startEngine() {
return `${this.type} engine started with ${this.#engineType} engine.`;
}
}
const myVehicle = new Vehicle('Car', 'V8');
console.log(myVehicle.startEngine()); // Output: Car engine started with V8 engine.
// console.log(myVehicle.#engineType); // Uncommenting this line will throw an error
In this example, the #engineType
property is private and cannot be accessed outside the Vehicle
class.
Private Methods
Private methods are also denoted by a #
symbol. They can only be called within the class.
class Vehicle {
#engineType;
constructor(type, engineType) {
this.type = type;
this.#engineType = engineType;
}
#privateMethod() {
return `${this.type} engine is private.`;
}
startEngine() {
return `${this.#engineType} engine started. ${this.#privateMethod()}`;
}
}
const myVehicle = new Vehicle('Car', 'V8');
console.log(myVehicle.startEngine()); // Output: V8 engine started. Car engine is private.
// myVehicle.#engineType; // Uncommenting this line will throw an error
// myVehicle.#privateMethod(); // Uncommenting this line will throw an error
In this example, the #engineType
property and #privateMethod
method are private and cannot be accessed outside the Vehicle
class.
Extending Built-in Objects
In ES6, you can extend built-in objects like Array
and Object
to create custom classes. This is particularly useful when you want to add custom functionality to built-in types.
Extending Array
You can extend the Array
object to create custom array-based classes.
class MyArray extends Array {
first() {
return this[0];
}
last() {
return this[this.length - 1];
}
}
const myArray = new MyArray(1, 2, 3, 4, 5);
console.log(myArray.first()); // Output: 1
console.log(myArray.last()); // Output: 5
In this example, the MyArray
class extends the built-in Array
object and adds two methods: first
and last
, which return the first and last elements of the array, respectively.
Extending Object
While you can extend Object
, it’s less common because Object
is a base class for almost all JavaScript objects. However, it can be useful in certain scenarios.
class MyObject extends Object {
constructor(name, value) {
super();
this.name = name;
this.value = value;
}
printInfo() {
return `${this.name}: ${this.value}`;
}
}
const myObject = new MyObject('Speed', 'Fast');
console.log(myObject.printInfo()); // Output: Speed: Fast
In this example, the MyObject
class extends the built-in Object
class and adds a printInfo
method to display the object's properties.
Extending Custom Classes
You can also extend custom classes to create a hierarchy of objects. This is useful for creating complex applications with a clear and organized structure.
class Vehicle {
constructor(type) {
this.type = type;
}
startEngine() {
return `${this.type} engine started.`;
}
}
class Car extends Vehicle {
constructor(make, model, year, type) {
super(type);
this.make = make;
this.model = model;
this.year = year;
}
displayInfo() {
return `This car is a ${this.year} ${this.make} ${this.model}.`;
}
}
const myCar = new Car('Toyota', 'Corolla', 2021, 'Car');
console.log(myCar.startEngine()); // Output: Car engine started.
console.log(myCar.displayInfo()); // Output: This car is a 2021 Toyota Corolla.
In this example, the Car
class extends the Vehicle
class, inheriting the startEngine
method and adding a displayInfo
method.
Overriding Methods
Overriding methods in a subclass allows you to modify or extend the behavior of methods inherited from the parent class.
Why Override Methods?
Overriding methods is useful when you need to provide a specific implementation for a method in a subclass, which might differ from the parent class’s implementation.
Example of Method Overriding
class Vehicle {
startEngine() {
return `Vehicle engine started.`;
}
}
class Car extends Vehicle {
startEngine() {
return `Car engine started. It is ready to drive.`;
}
}
const myCar = new Car();
console.log(myCar.startEngine()); // Output: Car engine started. It is ready to drive.
In this example, the Car
class overrides the startEngine
method from the Vehicle
class, providing a more specific implementation.
Summary and Recap
Key Takeaways
- Classes and inheritance in ES6 provide a clean and intuitive way to create objects and extend their functionality.
- The
super
keyword is used to call the parent class constructor and methods. - Private properties and methods provide encapsulation and protect the internal state of the class.
- You can extend built-in objects like
Array
andObject
to create custom classes. - Overriding methods allows you to provide specific implementations in subclasses.
Next Steps
Now that you have a solid understanding of ES6 classes and inheritance, you can apply these concepts to build scalable and maintainable JavaScript applications. Experiment with different use cases and explore more advanced features of JavaScript.
Recommended Resources
- MDN Web Docs on Classes: MDN Classes
- MDN Web Docs on Inheritance: MDN Inheritance
- JavaScript: The Good Parts by Douglas Crockford: This book provides a comprehensive introduction to JavaScript and its features, including classes and inheritance.
By mastering classes and inheritance, you’ll be well-equipped to tackle more complex JavaScript projects and write cleaner, more maintainable code. Happy coding!