JavaScript-Series-#34-Classes-and-Constructors-in-JavaScript
Welcome to another installment of our JavaScript series! In modern JavaScript development, understanding object-oriented programming (OOP) principles is key, and classes provide a clear, concise way to implement these principles. Introduced in ES6 (ECMAScript 2015), JavaScript classes offer a much-desired syntactic sugar over JavaScript's existing prototype-based inheritance model, making it easier for developers from class-based languages (like Java or C++) to write object-oriented code.
This post will dive deep into classes, their structure, how to initialize objects using constructors, and manage their behavior through methods.
What are JavaScript Classes?
At its core, a JavaScript class is a blueprint for creating objects. It encapsulates data (properties) and functions (methods) that operate on that data into a single unit. While they might look familiar to classes in other languages, it's crucial to remember they are still fundamentally built on JavaScript's prototypal inheritance model. They just provide a cleaner, more organized syntax to work with it.
Key benefits of using classes include:
- Improved Readability: Code becomes easier to understand and maintain.
- Organized Structure: Encapsulates related data and behavior.
- Reusability: Enables inheritance, allowing you to build upon existing class functionalities.
- Clarity: Provides a standard way to define object constructors and methods.
Basic Class Syntax
You declare a class using the class keyword, followed by the class name (typically capitalized by convention).
class Car {
// Class body goes here
}
// Creating an instance of the Car class
const myCar = new Car();
console.log(myCar); // Car {}
As you can see, simply declaring a class and creating an instance doesn't give us much yet. This is where the constructor comes into play.
The Constructor Method
The constructor is a special method within a class that JavaScript automatically calls when you create a new instance of the class using the new keyword. Its primary purpose is to initialize the object's properties.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false; // Default state
}
}
// Creating new car instances using the constructor
const car1 = new Car('Toyota', 'Camry', 2020);
const car2 = new Car('Honda', 'Civic', 2022);
console.log(car1);
// Car { make: 'Toyota', model: 'Camry', year: 2020, isRunning: false }
console.log(car2);
// Car { make: 'Honda', model: 'Civic', year: 2022, isRunning: false }
In the example above:
-
The
constructormethod takes three parameters:make,model, andyear. -
Inside the constructor,
thisrefers to the newly created instance of theCarclass. -
We assign the parameters to properties of the instance (e.g.,
this.make = make;). -
We also initialize a default property,
isRunning, demonstrating how you can set initial states.
Adding Instance Methods
Classes are not just about data; they also define the behaviors associated with that data. These behaviors are implemented as methods. Methods are functions defined inside the class body, but outside the constructor. They operate on the instance's data.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;
}
// Instance method to start the car
start() {
if (!this.isRunning) {
this.isRunning = true;
console.log(`${this.make} ${this.model} started.`);
} else {
console.log(`${this.make} ${this.model} is already running.`);
}
}
// Instance method to stop the car
stop() {
if (this.isRunning) {
this.isRunning = false;
console.log(`${this.make} ${this.model} stopped.`);
} else {
console.log(`${this.make} ${this.model} is already stopped.`);
}
}
// Instance method to get car info
getInfo() {
return `${this.year} ${this.make} ${this.model}`;
}
}
const myNewCar = new Car('Ford', 'Focus', 2021);
console.log(myNewCar.getInfo()); // 2021 Ford Focus
myNewCar.start(); // Ford Focus started.
myNewCar.start(); // Ford Focus is already running.
myNewCar.stop(); // Ford Focus stopped.
myNewCar.stop(); // Ford Focus is already stopped.
Notice how methods like start(), stop(), and getInfo() can access the instance's properties using this. Each instance of Car will have its own independent isRunning state.
Class Fields (Properties)
While properties are typically initialized in the constructor, modern JavaScript allows you to declare class fields directly within the class body. This can make the code cleaner, especially for properties with default values or for private properties.
Public Class Fields
These are accessible from outside the class instance.
class Dog {
breed = 'Golden Retriever'; // Public field with a default value
age = 1;
constructor(name) {
this.name = name;
}
bark() {
console.log(`${this.name} barks!`);
}
}
const myDog = new Dog('Buddy');
console.log(myDog.breed); // Golden Retriever
console.log(myDog.age); // 1
myDog.bark(); // Buddy barks!
Private Class Fields
A powerful feature for encapsulation, private class fields are denoted by a # prefix. They can only be accessed or modified from within the class itself.
class BankAccount {
#balance; // Private field
constructor(initialBalance) {
if (initialBalance < 0) {
throw new Error("Initial balance cannot be negative.");
}
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`Deposited ${amount}. New balance: ${this.#balance}`);
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
console.log(`Withdrew ${amount}. New balance: ${this.#balance}`);
} else {
console.log("Insufficient funds or invalid amount.");
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50); // Deposited 50. New balance: 150
account.withdraw(30); // Withdrew 30. New balance: 120
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Static Methods and Properties
Sometimes, you need functionality that belongs to the class itself, rather than to a specific instance of the class. This is where static methods and static properties come in. They are called directly on the class name, not on an instance.
class MathHelper {
static PI = 3.14159; // Static property
static add(a, b) { // Static method
return a + b;
}
static multiply(a, b) { // Static method
return a * b;
}
}
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.add(5, 3)); // 8
console.log(MathHelper.multiply(4, 2)); // 8
// const calculator = new MathHelper();
// calculator.add(1, 2); // TypeError: calculator.add is not a function
Static members are ideal for utility functions, factory methods, or constants that are related to the class concept but don't require an object's state to operate.
Inheritance with Classes (`extends` and `super`)
One of the cornerstones of OOP is inheritance, allowing a class to inherit properties and methods from another class. This promotes code reuse and helps in building hierarchical relationships.
-
The
extendskeyword is used to create a subclass. -
The
super()keyword is used inside the constructor of the subclass to call the parent class's constructor, ensuring that the parent's properties are initialized.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call the parent (Animal) constructor
this.breed = breed;
}
// Override parent method
speak() {
console.log(`${this.name} barks.`);
}
fetch() {
console.log(`${this.name} is fetching!`);
}
}
const myAnimal = new Animal('Generic Animal');
myAnimal.speak(); // Generic Animal makes a sound.
const myDoggo = new Dog('Max', 'Labrador');
myDoggo.speak(); // Max barks. (Overridden method)
myDoggo.fetch(); // Max is fetching!
console.log(myDoggo.name); // Max (inherited from Animal)
console.log(myDoggo.breed); // Labrador
This example demonstrates how Dog inherits from Animal, gains its name property and can override its speak method, while also adding its own unique behaviors like fetch.
Conclusion
JavaScript classes provide a powerful and intuitive way to structure your code, making it more readable, maintainable, and scalable. By understanding the roles of the class keyword, the constructor method, instance methods, class fields, static members, and inheritance patterns, you can effectively leverage OOP principles in your JavaScript applications. Embracing classes will undoubtedly enhance your ability to build robust and well-organized software.