Object-Oriented Programming (OOP) is a programming paradigm built around objects that combine data and behavior. JavaScript supports OOP through objects, prototypes, constructor functions, classes, inheritance, encapsulation, and polymorphism.
Understanding OOP becomes especially valuable when JavaScript applications grow beyond small scripts. Instead of keeping related variables and functions scattered across a codebase, OOP lets you organize them into reusable models with clear responsibilities.
In this article, we will build a practical banking example to understand classes, constructors, private fields, inheritance, method overriding, getters, and polymorphism. The example is designed to run directly in Node.js without external dependencies.
Understanding Object-Oriented Programming in JavaScript
A JavaScript class provides a convenient syntax for creating objects that share the same structure and behavior. A constructor initializes object-specific data, while methods define operations that the object can perform. Although the class syntax looks similar to class-based languages such as Java or C++, JavaScript classes are built on top of its prototype-based inheritance system.
Encapsulation is one of the most useful OOP concepts in real applications. JavaScript supports private class fields using the # syntax, allowing internal state to remain inaccessible from outside the object. This makes it possible to expose safe public methods while preventing consumers from directly modifying critical data.
Inheritance allows one class to reuse and extend another class. In the example below, SavingsAccount and BusinessAccount inherit common functionality from BankAccount but implement their own versions of the calculateInterest method. This demonstrates polymorphism because the same method call can produce different behavior depending on the object's actual type.
The final example also uses getters, validation, static methods, and detailed console output. Running it shows how objects are created, how encapsulated state changes through methods, and how polymorphic behavior works across different account types.
class BankAccount {
// Private fields cannot be accessed directly from outside the class.
#balance = 0;
constructor(owner, initialBalance = 0) {
this.owner = owner;
this.accountNumber = BankAccount.generateAccountNumber();
this.#balance = initialBalance;
}
// A getter provides controlled read-only access to private state.
get balance() {
return this.#balance;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("Deposit amount must be greater than zero.");
}
this.#balance += amount;
console.log(`[DEPOSIT] ${this.owner} deposited $${amount}.`);
console.log(`[BALANCE] Current balance: $${this.#balance}`);
}
withdraw(amount) {
if (amount <= 0) {
throw new Error("Withdrawal amount must be greater than zero.");
}
if (amount > this.#balance) {
throw new Error("Insufficient funds.");
}
this.#balance -= amount;
console.log(`[WITHDRAW] ${this.owner} withdrew $${amount}.`);
console.log(`[BALANCE] Current balance: $${this.#balance}`);
}
// Subclasses can override this method with specialized behavior.
calculateInterest() {
return 0;
}
showSummary() {
console.log(`\nAccount: ${this.accountNumber}`);
console.log(`Owner: ${this.owner}`);
console.log(`Balance: $${this.balance}`);
console.log(`Interest: $${this.calculateInterest()}`);
}
static generateAccountNumber() {
return `ACC-${Math.floor(100000 + Math.random() * 900000)}`;
}
}
class SavingsAccount extends BankAccount {
constructor(owner, initialBalance, interestRate = 0.04) {
super(owner, initialBalance);
this.interestRate = interestRate;
}
// Polymorphism: SavingsAccount provides its own implementation.
calculateInterest() {
return Number((this.balance * this.interestRate).toFixed(2));
}
}
class BusinessAccount extends BankAccount {
constructor(owner, initialBalance, interestRate = 0.02) {
super(owner, initialBalance);
this.interestRate = interestRate;
}
// Different account type, different interest calculation.
calculateInterest() {
return Number((this.balance * this.interestRate).toFixed(2));
}
}
console.log("=== OOP BANKING DEMO ===");
console.log("\n1. Creating objects from different classes...");
const savings = new SavingsAccount("Ansh", 5000, 0.05);
const business = new BusinessAccount("Acme Labs", 12000, 0.025);
console.log("Savings account created:", savings.accountNumber);
console.log("Business account created:", business.accountNumber);
console.log("\n2. Testing encapsulated state...");
savings.deposit(1000);
savings.withdraw(750);
console.log("\n3. Demonstrating polymorphism...");
const accounts = [savings, business];
for (const account of accounts) {
console.log(`\nChecking ${account.owner}'s account:`);
console.log(`Calculated interest: $${account.calculateInterest()}`);
}
console.log("\n4. Printing complete account summaries...");
savings.showSummary();
business.showSummary();
console.log("\n5. Demonstrating static class behavior...");
console.log("Generated account number:", BankAccount.generateAccountNumber());
console.log("\n=== DEMO COMPLETE ===");
Conclusion
Object-Oriented Programming gives JavaScript developers a practical way to model complex domains as interacting objects. Classes can encapsulate state, methods can protect business rules, and inheritance can provide shared behavior without duplicating implementation.
The most important lesson is that OOP is not simply about creating classes. Good OOP design focuses on clear responsibilities, controlled state, reusable behavior, and interfaces that make objects easy to work with.
JavaScript also gives you flexibility: you can combine classes with functional programming, modules, closures, and composition depending on the problem. As applications grow, understanding these trade-offs helps you choose an architecture that remains maintainable instead of forcing every feature into an inheritance hierarchy.
Top comments (0)