Chapter 3: JavaScript Classes - Complete Guide to Object-Oriented Programming
Classes in JavaScript provide a clean and intuitive way to work with object-oriented programming concepts. Introduced in ES6, classes offer a more familiar syntax for developers coming from other programming languages while maintaining JavaScript's prototypal inheritance under the hood.
Why Classes Matter in JavaScript
Classes in JavaScript are essential because they:
- Organize Code: Group related data and behavior together
- Enable Inheritance: Create hierarchical relationships between objects
- Improve Readability: Provide clear, structured code organization
- Support Encapsulation: Control access to object properties and methods
- Facilitate Reusability: Create reusable object blueprints
Learning Objectives
Through this chapter, you will master:
- Class syntax and constructor methods
- Instance methods and properties
- Static methods and properties
- Class inheritance and super keyword
- Private fields and methods
- Getters and setters
Basic Class Syntax
Class Declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
}
getAge() {
return this.age;
}
}
// Creating instances
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
console.log(person1.greet()); // Hello, I'm Alice and I'm 30 years old.
console.log(person2.getAge()); // 25
Class Expression
// Named class expression
const Person = class PersonClass {
constructor(name) {
this.name = name;
}
};
// Anonymous class expression
const Animal = class {
constructor(species) {
this.species = species;
}
};
const dog = new Animal("Dog");
console.log(dog.species); // Dog
Constructor and Instance Methods
Constructor Method
The constructor is a special method that runs when a new instance is created:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
this.area = width * height; // Can calculate during construction
}
// Instance method
getArea() {
return this.width * this.height;
}
// Method that modifies instance
resize(newWidth, newHeight) {
this.width = newWidth;
this.height = newHeight;
this.area = newWidth * newHeight;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.getArea()); // 50
rect.resize(15, 10);
console.log(rect.getArea()); // 150
Method Chaining
class Calculator {
constructor(value = 0) {
this.value = value;
}
add(num) {
this.value += num;
return this; // Return this for chaining
}
subtract(num) {
this.value -= num;
return this;
}
multiply(num) {
this.value *= num;
return this;
}
getResult() {
return this.value;
}
}
const calc = new Calculator(10)
.add(5)
.multiply(2)
.subtract(3);
console.log(calc.getResult()); // 27
Static Methods and Properties
Static methods and properties belong to the class itself, not to instances:
class MathUtils {
// Static property
static PI = 3.14159;
// Static method
static circleArea(radius) {
return this.PI * radius * radius;
}
static max(a, b) {
return a > b ? a : b;
}
// Instance method
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
// Using static methods and properties
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.circleArea(5)); // 78.54
console.log(MathUtils.max(10, 20)); // 20
// Static methods are not available on instances
const math = new MathUtils(42);
// math.circleArea(5); // Error: math.circleArea is not a function
console.log(math.getValue()); // 42
Class Inheritance
Basic Inheritance
// Parent class
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
speak() {
return `${this.name} makes a sound.`;
}
getInfo() {
return `${this.name} is a ${this.species}.`;
}
}
// Child class
class Dog extends Animal {
constructor(name, breed) {
super(name, "dog"); // Call parent constructor
this.breed = breed;
}
// Override parent method
speak() {
return `${this.name} barks!`;
}
// New method specific to Dog
fetch() {
return `${this.name} fetches the ball!`;
}
}
const dog = new Dog("Buddy", "Golden Retriever");
console.log(dog.speak()); // Buddy barks!
console.log(dog.getInfo()); // Buddy is a dog.
console.log(dog.fetch()); // Buddy fetches the ball!
Super Keyword
class Vehicle {
constructor(brand, year) {
this.brand = brand;
this.year = year;
}
start() {
return `${this.brand} vehicle started.`;
}
getAge() {
return new Date().getFullYear() - this.year;
}
}
class Car extends Vehicle {
constructor(brand, year, doors) {
super(brand, year); // Must call super before using this
this.doors = doors;
}
start() {
// Call parent method and extend it
return super.start() + ` This car has ${this.doors} doors.`;
}
honk() {
return `${this.brand} car honks!`;
}
}
const car = new Car("Toyota", 2020, 4);
console.log(car.start()); // Toyota vehicle started. This car has 4 doors.
console.log(car.getAge()); // 4 (assuming current year is 2024)
console.log(car.honk()); // Toyota car honks!
Private Fields and Methods
Private Fields (ES2022)
class BankAccount {
// Private fields (prefixed with #)
#balance = 0;
#accountNumber;
constructor(accountNumber, initialBalance = 0) {
this.#accountNumber = accountNumber;
this.#balance = initialBalance;
}
// Public methods
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return `Deposited $${amount}. New balance: $${this.#balance}`;
}
return "Invalid deposit amount";
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return `Withdrew $${amount}. New balance: $${this.#balance}`;
}
return "Insufficient funds or invalid amount";
}
getBalance() {
return this.#balance;
}
getAccountNumber() {
return this.#accountNumber;
}
// Private method
#validateAmount(amount) {
return typeof amount === 'number' && amount > 0;
}
}
const account = new BankAccount("12345", 1000);
console.log(account.deposit(500)); // Deposited $500. New balance: $1500
console.log(account.getBalance()); // 1500
// Private fields are not accessible from outside
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Private Methods
class User {
#password;
#email;
constructor(username, email, password) {
this.username = username;
this.#email = email;
this.#password = password;
}
// Public method
login(inputPassword) {
if (this.#validatePassword(inputPassword)) {
return `Welcome back, ${this.username}!`;
}
return "Invalid password";
}
// Private method
#validatePassword(inputPassword) {
return inputPassword === this.#password;
}
// Public method that uses private method
changePassword(oldPassword, newPassword) {
if (this.#validatePassword(oldPassword)) {
this.#password = newPassword;
return "Password changed successfully";
}
return "Invalid old password";
}
}
const user = new User("john_doe", "[email protected]", "secret123");
console.log(user.login("secret123")); // Welcome back, john_doe!
console.log(user.login("wrong")); // Invalid password
Getters and Setters
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
// Getter
get celsius() {
return this._celsius;
}
// Setter
set celsius(value) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero");
}
this._celsius = value;
}
// Getter for computed property
get fahrenheit() {
return (this._celsius * 9/5) + 32;
}
// Setter for computed property
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}
get kelvin() {
return this._celsius + 273.15;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
console.log(temp.fahrenheit); // 86
// Using setter with validation
try {
temp.celsius = -300; // This will throw an error
} catch (error) {
console.log(error.message); // Temperature cannot be below absolute zero
}
Advanced Class Features
Class Fields (Public and Private)
class Counter {
// Public class field
static totalCounters = 0;
// Private class field
static #maxCounters = 10;
// Public instance field
count = 0;
// Private instance field
#step = 1;
constructor(initialCount = 0, step = 1) {
this.count = initialCount;
this.#step = step;
Counter.totalCounters++;
}
increment() {
this.count += this.#step;
}
decrement() {
this.count -= this.#step;
}
static canCreateMore() {
return this.totalCounters < this.#maxCounters;
}
static getMaxCounters() {
return this.#maxCounters;
}
}
const counter1 = new Counter(0, 2);
const counter2 = new Counter(10, 5);
console.log(Counter.totalCounters); // 2
console.log(Counter.canCreateMore()); // true
console.log(Counter.getMaxCounters()); // 10
counter1.increment();
console.log(counter1.count); // 2
Mixins
// Mixin for logging functionality
const Loggable = {
log(message) {
console.log(`[${this.constructor.name}] ${message}`);
}
};
// Mixin for serialization
const Serializable = {
toJSON() {
return JSON.stringify(this);
}
};
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// Apply mixins
Object.assign(User.prototype, Loggable, Serializable);
const user = new User("John", "[email protected]");
user.log("User created"); // [User] User created
console.log(user.toJSON()); // {"name":"John","email":"[email protected]"}
Best Practices
1. Use Classes for Complex Objects
// Good: Use classes for complex objects with behavior
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// Avoid: Don't use classes for simple data structures
const simpleData = {
name: "John",
age: 30
};
2. Prefer Composition Over Inheritance
// Good: Composition
class Engine {
start() {
return "Engine started";
}
}
class Car {
constructor() {
this.engine = new Engine();
}
start() {
return this.engine.start();
}
}
// Avoid: Deep inheritance chains
class Vehicle { }
class MotorVehicle extends Vehicle { }
class Car extends MotorVehicle { }
class SportsCar extends Car { }
3. Use Private Fields for Encapsulation
class BankAccount {
#balance = 0;
#transactions = [];
deposit(amount) {
this.#balance += amount;
this.#transactions.push({ type: 'deposit', amount, date: new Date() });
}
getBalance() {
return this.#balance;
}
getTransactionHistory() {
return [...this.#transactions]; // Return a copy
}
}
Summary
JavaScript classes provide a powerful and intuitive way to work with object-oriented programming:
- Class Syntax: Use
class
keyword with constructor and methods - Inheritance: Extend classes with
extends
and usesuper
- Static Members: Use
static
for class-level properties and methods - Private Fields: Use
#
prefix for encapsulation - Getters/Setters: Control property access and validation
- Best Practices: Use classes for complex objects, prefer composition, and leverage private fields
Classes in JavaScript are syntactic sugar over prototypal inheritance, making OOP concepts more accessible while maintaining JavaScript's flexibility and power.
This tutorial is part of the JavaScript Mastery series by syscook.dev