HTML & JS Topic
Object-Oriented JavaScript
The Evolution: Constructor Functions → Classes
1. Constructor Functions (Traditional OOP in JS)
javascript
// Basic constructor function
function Person(name, age) {
// Properties
this.name = name;
this.age = age;
// Method
this.greet = function() {
return `Hello, my name is ${this.name}`;
};
}
// Creating instances
const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);
console.log(alice.name); // "Alice"
console.log(bob.greet()); // "Hello, my name is Bob"
// Checking type
console.log(alice instanceof Person); // true
console.log(alice.constructor === Person); // true
Constructor Function Examples:
javascript
// Product constructor
function Product(name, price, category) {
this.id = Date.now(); // Auto-generated ID
this.name = name;
this.price = price;
this.category = category;
// Format price as currency
this.formatPrice = function() {
return `$${this.price.toFixed(2)}`;
};
// Apply discount
this.applyDiscount = function(percent) {
if (percent > 0 && percent <= 100) {
this.price *= (100 - percent) / 100;
return true;
}
return false;
};
}
// Usage
const laptop = new Product('Laptop', 999.99, 'Electronics');
const phone = new Product('Phone', 699.99, 'Electronics');
console.log(laptop.formatPrice()); // "$999.99"
laptop.applyDiscount(10);
console.log(laptop.formatPrice()); // "$899.99"
// User constructor with validation
function User(username, email, role = 'user') {
// Validation
if (!username || username.length < 3) {
throw new Error('Username must be at least 3 characters');
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email address');
}
// Private variables (convention with underscore)
let _password = null;
let _isActive = true;
// Public properties
this.username = username;
this.email = email;
this.role = role;
this.createdAt = new Date();
// Public methods
this.setPassword = function(password) {
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
_password = btoa(password); // Simple encoding (not secure for production)
};
this.checkPassword = function(password) {
return btoa(password) === _password;
};
this.deactivate = function() {
_isActive = false;
console.log(`${this.username} has been deactivated`);
};
this.isActive = function() {
return _isActive;
};
// Getter method
this.getInfo = function() {
return {
username: this.username,
email: this.email,
role: this.role,
isActive: _isActive,
createdAt: this.createdAt.toISOString().split('T')[0]
};
};
}
// Usage
try {
const user1 = new User('john_doe', 'john@example.com', 'admin');
user1.setPassword('secure123');
console.log(user1.checkPassword('secure123')); // true
console.log(user1.getInfo());
} catch (error) {
console.error('Error creating user:', error.message);
}
Issues with Constructor Functions:
javascript
// Problem 1: Each instance gets its own copy of methods
function Person(name) {
this.name = name;
this.sayHi = function() { // New function created for each instance!
return `Hi, I'm ${this.name}`;
};
}
const p1 = new Person('Alice');
const p2 = new Person('Bob');
console.log(p1.sayHi === p2.sayHi); // false - different function objects!
// Problem 2: No private properties (convention only)
function Account(balance) {
let _balance = balance; // Supposed to be private
this.getBalance = function() {
return _balance; // Closure gives access
};
}
const acc = new Account(1000);
console.log(acc._balance); // undefined (good!)
console.log(acc.getBalance()); // 1000
// But still accessible through other means...
Classes (Modern ES6 OOP)
Basic Class Syntax:
javascript
// Class declaration
class Person {
// Constructor - called when creating new instance
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance method
greet() {
return `Hello, my name is ${this.name} and I'm ${this.age} years old`;
}
// Another method
haveBirthday() {
this.age++;
console.log(`Happy birthday ${this.name}! You're now ${this.age}`);
return this.age;
}
}
// Creating instances
const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);
console.log(alice.greet()); // "Hello, my name is Alice..."
console.log(bob.haveBirthday()); // 31
// Methods are shared across instances
console.log(alice.greet === bob.greet); // true - same function!
Class Examples:
javascript
// Product class
class Product {
constructor(name, price, category) {
this.name = name;
this.price = price;
this.category = category;
this.createdAt = new Date();
this.isAvailable = true;
}
// Instance methods
formatPrice() {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.price);
}
applyDiscount(percent) {
if (percent > 0 && percent <= 100) {
const discount = this.price * (percent / 100);
this.price -= discount;
return discount;
}
throw new Error('Discount percentage must be between 0 and 100');
}
toggleAvailability() {
this.isAvailable = !this.isAvailable;
return this.isAvailable;
}
// Getter method
get productInfo() {
return `${this.name} (${this.category}) - ${this.formatPrice()}`;
}
}
// Usage
const laptop = new Product('MacBook Pro', 1999.99, 'Electronics');
console.log(laptop.formatPrice()); // "$1,999.99"
console.log(laptop.productInfo); // "MacBook Pro (Electronics) - $1,999.99"
laptop.applyDiscount(15);
console.log(laptop.formatPrice()); // "$1,699.99"
// Shopping Cart class
class ShoppingCart {
constructor() {
this.items = [];
this.total = 0;
this.lastUpdated = new Date();
}
addItem(product, quantity = 1) {
const existingItem = this.items.find(item =>
item.product.name === product.name
);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({
product,
quantity,
addedAt: new Date()
});
}
this.calculateTotal();
this.updateTimestamp();
console.log(`Added ${quantity}x ${product.name} to cart`);
}
removeItem(productName, quantity = 1) {
const itemIndex = this.items.findIndex(
item => item.product.name === productName
);
if (itemIndex === -1) {
console.log(`Product "${productName}" not found in cart`);
return false;
}
const item = this.items[itemIndex];
if (item.quantity <= quantity) {
this.items.splice(itemIndex, 1);
console.log(`Removed ${productName} from cart`);
} else {
item.quantity -= quantity;
console.log(`Removed ${quantity}x ${productName} from cart`);
}
this.calculateTotal();
this.updateTimestamp();
return true;
}
calculateTotal() {
this.total = this.items.reduce((sum, item) => {
return sum + (item.product.price * item.quantity);
}, 0);
}
applyCoupon(discountPercent) {
if (discountPercent > 0 && discountPercent <= 100) {
const discount = this.total * (discountPercent / 100);
this.total -= discount;
console.log(`Applied ${discountPercent}% coupon: -$${discount.toFixed(2)}`);
return discount;
}
throw new Error('Invalid discount percentage');
}
clearCart() {
this.items = [];
this.total = 0;
this.updateTimestamp();
console.log('Cart cleared');
}
updateTimestamp() {
this.lastUpdated = new Date();
}
// Getter for formatted total
get formattedTotal() {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.total);
}
// Getter for item count
get itemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
// Method to display cart summary
getSummary() {
return {
itemCount: this.itemCount,
total: this.formattedTotal,
items: this.items.map(item => ({
name: item.product.name,
quantity: item.quantity,
price: item.product.formatPrice(),
subtotal: (item.product.price * item.quantity).toFixed(2)
})),
lastUpdated: this.lastUpdated.toLocaleString()
};
}
}
// Usage
const cart = new ShoppingCart();
const product1 = new Product('Laptop', 999.99, 'Electronics');
const product2 = new Product('Mouse', 29.99, 'Electronics');
cart.addItem(product1, 1);
cart.addItem(product2, 2);
cart.addItem(product1, 1); // Adds to existing item
console.log(cart.itemCount); // 4
console.log(cart.formattedTotal); // "$1,059.97"
console.log(cart.getSummary());
cart.applyCoupon(10);
console.log(cart.formattedTotal); // "$953.97"
Inheritance
Constructor Function Inheritance:
javascript
// Parent constructor
function Animal(name, species) {
this.name = name;
this.species = species;
this.isAlive = true;
}
// Add methods to prototype (shared across instances)
Animal.prototype.speak = function() {
return `${this.name} the ${this.species} makes a sound`;
};
Animal.prototype.eat = function(food) {
return `${this.name} eats ${food}`;
};
// Child constructor
function Dog(name, breed) {
// Call parent constructor with current 'this'
Animal.call(this, name, 'Dog');
this.breed = breed;
}
// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Child-specific methods
Dog.prototype.speak = function() {
return `${this.name} barks!`;
};
Dog.prototype.fetch = function(item) {
return `${this.name} fetches the ${item}`;
};
// Another child
function Cat(name, color) {
Animal.call(this, name, 'Cat');
this.color = color;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function() {
return `${this.name} meows!`;
};
Cat.prototype.scratch = function() {
return `${this.name} scratches the furniture`;
};
// Usage
const rex = new Dog('Rex', 'German Shepherd');
const whiskers = new Cat('Whiskers', 'Orange');
console.log(rex.speak()); // "Rex barks!"
console.log(rex.eat('bone')); // "Rex eats bone" (inherited)
console.log(rex.fetch('ball')); // "Rex fetches the ball"
console.log(whiskers.speak()); // "Whiskers meows!"
console.log(whiskers.scratch()); // "Whiskers scratches the furniture"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Cat); // false
Class Inheritance (ES6):
javascript
// Parent class
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.speed = 0;
this.isRunning = false;
}
start() {
if (!this.isRunning) {
this.isRunning = true;
console.log(`${this.make} ${this.model} started`);
return true;
}
console.log(`${this.make} ${this.model} is already running`);
return false;
}
stop() {
if (this.isRunning) {
this.isRunning = false;
this.speed = 0;
console.log(`${this.make} ${this.model} stopped`);
return true;
}
console.log(`${this.make} ${this.model} is already stopped`);
return false;
}
accelerate(speedIncrease) {
if (this.isRunning) {
this.speed += speedIncrease;
console.log(`${this.make} ${this.model} accelerated to ${this.speed} km/h`);
return this.speed;
}
console.log(`Cannot accelerate - vehicle is not running`);
return this.speed;
}
brake(speedDecrease) {
const newSpeed = Math.max(0, this.speed - speedDecrease);
this.speed = newSpeed;
console.log(`${this.make} ${this.model} slowed to ${this.speed} km/h`);
if (this.speed === 0 && this.isRunning) {
this.stop();
}
return this.speed;
}
// Getter
get vehicleInfo() {
return `${this.year} ${this.make} ${this.model}`;
}
// Static method - called on class, not instance
static compareYears(vehicle1, vehicle2) {
return vehicle1.year - vehicle2.year;
}
}
// Child class - Car
class Car extends Vehicle {
constructor(make, model, year, fuelType, doors) {
super(make, model, year); // Call parent constructor
this.fuelType = fuelType;
this.doors = doors;
this.fuelLevel = 100; // percentage
}
// Override parent method
start() {
if (this.fuelLevel <= 0) {
console.log(`Cannot start - out of fuel`);
return false;
}
return super.start(); // Call parent method
}
// New method specific to Car
refuel(amount) {
this.fuelLevel = Math.min(100, this.fuelLevel + amount);
console.log(`Refueled to ${this.fuelLevel}%`);
return this.fuelLevel;
}
// Override with additional behavior
accelerate(speedIncrease) {
if (this.fuelLevel <= 0) {
console.log(`Cannot accelerate - out of fuel`);
return this.speed;
}
// Use 1% fuel per 10 km/h acceleration
const fuelUsed = speedIncrease / 10;
this.fuelLevel = Math.max(0, this.fuelLevel - fuelUsed);
if (this.fuelLevel <= 0) {
console.log(`Ran out of fuel!`);
this.stop();
}
return super.accelerate(speedIncrease);
}
// Getter specific to Car
get carInfo() {
return `${super.vehicleInfo} - ${this.doors}-door ${this.fuelType} car`;
}
}
// Another child class - Motorcycle
class Motorcycle extends Vehicle {
constructor(make, model, year, type) {
super(make, model, year);
this.type = type; // e.g., 'sport', 'cruiser', 'touring'
this.isKickstandUp = false;
}
// Override with additional step
start() {
if (!this.isKickstandUp) {
console.log(`Cannot start - kickstand is down`);
return false;
}
return super.start();
}
// New method
liftKickstand() {
this.isKickstandUp = true;
console.log(`Kickstand lifted`);
return true;
}
lowerKickstand() {
this.isKickstandUp = false;
console.log(`Kickstand lowered`);
return false;
}
// New method - wheelie!
doWheelie() {
if (this.speed > 30 && this.isRunning) {
console.log(`${this.make} ${this.model} does a wheelie!`);
return true;
}
console.log(`Not fast enough for a wheelie`);
return false;
}
}
// Usage
const sedan = new Car('Toyota', 'Camry', 2022, 'gasoline', 4);
const harley = new Motorcycle('Harley-Davidson', 'Sportster', 2023, 'cruiser');
sedan.start(); // "Toyota Camry started"
sedan.accelerate(50); // Accelerates, uses fuel
console.log(sedan.carInfo); // "2022 Toyota Camry - 4-door gasoline car"
harley.liftKickstand();
harley.start(); // "Harley-Davidson Sportster started"
harley.accelerate(40);
harley.doWheelie(); // "Harley-Davidson Sportster does a wheelie!"
// Static method usage
console.log(Vehicle.compareYears(sedan, harley)); // -1 (sedan is older)
// Check inheritance
console.log(sedan instanceof Car); // true
console.log(sedan instanceof Vehicle); // true
console.log(harley instanceof Motorcycle); // true
console.log(harley instanceof Car); // false
Multi-level Inheritance:
javascript
// Base class
class Employee {
constructor(name, id, department) {
this.name = name;
this.id = id;
this.department = department;
this.hireDate = new Date();
this.salary = 0;
}
work() {
return `${this.name} is working in ${this.department}`;
}
takeBreak() {
return `${this.name} is taking a break`;
}
getEmployeeInfo() {
return {
name: this.name,
id: this.id,
department: this.department,
hireDate: this.hireDate.toISOString().split('T')[0]
};
}
}
// Manager extends Employee
class Manager extends Employee {
constructor(name, id, department, teamSize) {
super(name, id, department);
this.teamSize = teamSize;
this.teamMembers = [];
}
// Override work method
work() {
return `${super.work()} and managing a team of ${this.teamSize}`;
}
// New methods
hireEmployee(employee) {
this.teamMembers.push(employee);
console.log(`${employee.name} hired to ${this.department}`);
return this.teamMembers.length;
}
conductMeeting() {
return `${this.name} is conducting a team meeting with ${this.teamMembers.length} members`;
}
// Set salary with validation
setSalary(amount) {
if (amount >= 50000) {
this.salary = amount;
return true;
}
throw new Error('Manager salary must be at least $50,000');
}
}
// Director extends Manager
class Director extends Manager {
constructor(name, id, department, teamSize, budget) {
super(name, id, department, teamSize);
this.budget = budget;
this.managers = [];
}
// Override work method
work() {
return `${super.work()}, overseeing a budget of $${this.budget.toLocaleString()}`;
}
// New methods
approveBudget(amount) {
if (amount <= this.budget) {
this.budget -= amount;
console.log(`Budget of $${amount} approved. Remaining: $${this.budget}`);
return true;
}
console.log(`Budget request of $${amount} denied. Insufficient funds.`);
return false;
}
hireManager(manager) {
this.managers.push(manager);
console.log(`${manager.name} hired as manager`);
return this.managers.length;
}
// Getter for total team size
get totalTeamSize() {
const managerTeams = this.managers.reduce((total, manager) =>
total + manager.teamSize, 0
);
return this.teamSize + managerTeams;
}
}
// Usage
const developer = new Employee('Alice', 'E001', 'Engineering');
const manager = new Manager('Bob', 'M001', 'Engineering', 5);
const director = new Director('Carol', 'D001', 'Technology', 3, 1000000);
console.log(developer.work()); // "Alice is working in Engineering"
console.log(manager.work()); // "Bob is working in Engineering and managing a team of 5"
console.log(director.work()); // "Carol is working in Technology and managing a team of 3, overseeing a budget of $1,000,000"
manager.hireEmployee(developer);
console.log(manager.conductMeeting()); // "Bob is conducting a team meeting with 1 members"
director.hireManager(manager);
console.log(director.totalTeamSize); // 8 (3 direct + 5 from manager)
director.approveBudget(50000); // Budget approved
Encapsulation
Private Fields and Methods (Modern):
javascript
// Using # for truly private fields and methods (ES2022)
class BankAccount {
// Private fields (start with #)
#accountNumber;
#balance;
#pin;
#transactionHistory = [];
constructor(accountHolder, initialBalance = 0, pin = '0000') {
this.accountHolder = accountHolder;
this.#accountNumber = this.#generateAccountNumber();
this.#balance = initialBalance;
this.#pin = pin;
this.isActive = true;
this.#logTransaction('Account opened', initialBalance);
}
// Private method
#generateAccountNumber() {
return 'ACC' + Math.random().toString().slice(2, 10);
}
// Private method
#validatePin(pin) {
return this.#pin === pin;
}
// Private method
#logTransaction(type, amount) {
this.#transactionHistory.push({
type,
amount,
date: new Date().toISOString(),
balance: this.#balance
});
}
// Public methods
deposit(amount, pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
this.#balance += amount;
this.#logTransaction('Deposit', amount);
console.log(`Deposited $${amount}. New balance: $${this.#balance}`);
return this.#balance;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
this.#logTransaction('Withdrawal', -amount);
console.log(`Withdrew $${amount}. New balance: $${this.#balance}`);
return this.#balance;
}
transfer(amount, targetAccount, pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
this.withdraw(amount, pin); // This will validate and deduct
targetAccount.deposit(amount, targetAccount.#pin); // Simplified
this.#logTransaction('Transfer to ' + targetAccount.accountHolder, -amount);
console.log(`Transferred $${amount} to ${targetAccount.accountHolder}`);
}
// Getter for balance (read-only access)
getBalance(pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
return this.#balance;
}
// Getter for formatted account info
get accountInfo() {
return {
accountHolder: this.accountHolder,
accountNumber: this.#accountNumber,
isActive: this.isActive
// Note: balance and PIN not included!
};
}
// Method to get recent transactions (with PIN)
getRecentTransactions(pin, limit = 5) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
return this.#transactionHistory
.slice(-limit)
.map(tx => ({
...tx,
date: new Date(tx.date).toLocaleString()
}));
}
// Static method
static validateAccountNumber(accNumber) {
return /^ACC\d{8}$/.test(accNumber);
}
}
// Usage
const account1 = new BankAccount('Alice', 1000, '1234');
const account2 = new BankAccount('Bob', 500, '5678');
console.log(account1.accountInfo);
// { accountHolder: 'Alice', accountNumber: 'ACC12345678', isActive: true }
try {
account1.deposit(500, '1234'); // OK
console.log(account1.getBalance('1234')); // 1500
account1.withdraw(200, '1234'); // OK
console.log(account1.getBalance('1234')); // 1300
// Transfer
account1.transfer(300, account2, '1234');
// Try to access private field (will fail)
// console.log(account1.#balance); // SyntaxError
// Get transactions
console.log(account1.getRecentTransactions('1234', 3));
} catch (error) {
console.error('Bank error:', error.message);
}
Encapsulation with Closures (Pre-ES2022):
javascript
// Using IIFE and closures for encapsulation
const User = (function() {
// Private variables (shared across all instances)
let userCount = 0;
// Private helper function
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Return class definition
return class User {
constructor(username, email) {
// Public properties
this.username = username;
// Private variables (instance-specific)
let _email = '';
let _password = null;
let _isVerified = false;
// Set email with validation
this.setEmail = function(email) {
if (validateEmail(email)) {
_email = email;
return true;
}
throw new Error('Invalid email address');
};
// Get email
this.getEmail = function() {
return _email;
};
// Set password
this.setPassword = function(password) {
if (password.length >= 8) {
_password = password; // In real app, hash this!
return true;
}
throw new Error('Password must be at least 8 characters');
};
// Verify password
this.verifyPassword = function(password) {
return _password === password;
};
// Verification
this.verify = function() {
_isVerified = true;
console.log(`${username} has been verified`);
};
this.isVerified = function() {
return _isVerified;
};
// Public method that uses private data
this.getUserInfo = function() {
return {
username: this.username,
email: _email,
isVerified: _isVerified,
userNumber: ++userCount
};
};
// Initialize with provided email
if (email) {
this.setEmail(email);
}
}
// Public method (shared across instances)
greet() {
return `Hello, I'm ${this.username}`;
}
// Static method
static getUserCount() {
return userCount;
}
};
})();
// Usage
const user1 = new User('alice123', 'alice@example.com');
const user2 = new User('bob456');
user1.setPassword('secure123');
user1.verify();
console.log(user1.getUserInfo());
// { username: 'alice123', email: 'alice@example.com', isVerified: true, userNumber: 1 }
console.log(user1.verifyPassword('secure123')); // true
console.log(user1.greet()); // "Hello, I'm alice123"
console.log(User.getUserCount()); // 2
Complete Example: E-commerce System
javascript
// Base Product class with encapsulation
class Product {
#id;
#name;
#price;
#stock;
#category;
constructor(name, price, category, stock = 0) {
if (price <= 0) {
throw new Error('Price must be positive');
}
this.#id = `PROD_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.#name = name;
this.#price = price;
this.#category = category;
this.#stock = stock;
this.createdAt = new Date();
this.lastUpdated = new Date();
}
// Getters
get id() { return this.#id; }
get name() { return this.#name; }
get price() { return this.#price; }
get stock() { return this.#stock; }
get category() { return this.#category; }
// Setters with validation
set name(newName) {
if (newName && newName.trim().length >= 2) {
this.#name = newName.trim();
this.#updateTimestamp();
} else {
throw new Error('Product name must be at least 2 characters');
}
}
set price(newPrice) {
if (newPrice > 0) {
this.#price = newPrice;
this.#updateTimestamp();
} else {
throw new Error('Price must be positive');
}
}
set stock(newStock) {
if (newStock >= 0) {
this.#stock = newStock;
this.#updateTimestamp();
} else {
throw new Error('Stock cannot be negative');
}
}
// Private method
#updateTimestamp() {
this.lastUpdated = new Date();
}
// Public methods
applyDiscount(percent) {
if (percent >= 0 && percent <= 100) {
const discount = this.#price * (percent / 100);
this.#price = this.#price - discount;
this.#updateTimestamp();
return discount;
}
throw new Error('Discount must be between 0 and 100');
}
addStock(quantity) {
this.stock += quantity;
return this.stock;
}
removeStock(quantity) {
if (quantity > this.stock) {
throw new Error('Insufficient stock');
}
this.stock -= quantity;
return this.stock;
}
getFormattedPrice() {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.#price);
}
getProductInfo() {
return {
id: this.#id,
name: this.#name,
price: this.getFormattedPrice(),
category: this.#category,
stock: this.#stock,
lastUpdated: this.lastUpdated.toLocaleString()
};
}
// Static factory method
static createFromObject(data) {
return new Product(
data.name,
data.price,
data.category,
data.stock || 0
);
}
}
// DigitalProduct extends Product
class DigitalProduct extends Product {
#fileSize;
#downloadUrl;
#downloadCount = 0;
constructor(name, price, category, fileSize, downloadUrl) {
super(name, price, category, 0); // Digital products have unlimited "stock"
this.#fileSize = fileSize;
this.#downloadUrl = downloadUrl;
}
// Override stock handling for digital products
addStock() {
console.log('Digital products have unlimited stock');
return Infinity;
}
removeStock() {
console.log('Digital products cannot be removed from stock');
return Infinity;
}
// New methods for digital products
download() {
this.#downloadCount++;
console.log(`Downloading ${this.name} (${this.#formatFileSize()})`);
return {
url: this.#downloadUrl,
downloadId: `DL_${Date.now()}`,
count: this.#downloadCount
};
}
// Private method
#formatFileSize() {
const units = ['B', 'KB', 'MB', 'GB'];
let size = this.#fileSize;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
// Override getProductInfo
getProductInfo() {
const baseInfo = super.getProductInfo();
return {
...baseInfo,
type: 'digital',
fileSize: this.#formatFileSize(),
downloadCount: this.#downloadCount,
stock: 'Unlimited'
};
}
}
// Inventory management system
class Inventory {
#products = new Map(); // Store products by ID
#categories = new Set();
addProduct(product) {
if (!(product instanceof Product)) {
throw new Error('Only Product instances can be added');
}
this.#products.set(product.id, product);
this.#categories.add(product.category);
console.log(`Added product: ${product.name}`);
return product.id;
}
removeProduct(productId) {
if (this.#products.has(productId)) {
const product = this.#products.get(productId);
this.#products.delete(productId);
// Remove category if no products left in it
const categoryProducts = this.getProductsByCategory(product.category);
if (categoryProducts.length === 0) {
this.#categories.delete(product.category);
}
console.log(`Removed product: ${product.name}`);
return true;
}
return false;
}
getProduct(productId) {
return this.#products.get(productId) || null;
}
getProductsByCategory(category) {
return Array.from(this.#products.values())
.filter(product => product.category === category);
}
searchProducts(query) {
const searchTerm = query.toLowerCase();
return Array.from(this.#products.values()).filter(product =>
product.name.toLowerCase().includes(searchTerm) ||
product.category.toLowerCase().includes(searchTerm)
);
}
getLowStockProducts(threshold = 10) {
return Array.from(this.#products.values())
.filter(product => product.stock < threshold && product.stock > 0);
}
getOutOfStockProducts() {
return Array.from(this.#products.values())
.filter(product => product.stock === 0);
}
getTotalInventoryValue() {
return Array.from(this.#products.values())
.reduce((total, product) =>
total + (product.price * product.stock), 0
);
}
getInventorySummary() {
return {
totalProducts: this.#products.size,
totalCategories: this.#categories.size,
totalValue: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.getTotalInventoryValue()),
lowStock: this.getLowStockProducts().length,
outOfStock: this.getOutOfStockProducts().length,
categories: Array.from(this.#categories).sort()
};
}
// Static method to create inventory from array
static createFromArray(productsArray) {
const inventory = new Inventory();
productsArray.forEach(productData => {
try {
const product = Product.createFromObject(productData);
inventory.addProduct(product);
} catch (error) {
console.error(`Failed to add product: ${error.message}`);
}
});
return inventory;
}
}
// Usage example
const inventory = new Inventory();
// Add physical products
const laptop = new Product('Laptop', 999.99, 'Electronics', 15);
const mouse = new Product('Wireless Mouse', 29.99, 'Accessories', 50);
const book = new Product('JavaScript Guide', 39.99, 'Books', 3);
inventory.addProduct(laptop);
inventory.addProduct(mouse);
inventory.addProduct(book);
// Add digital product
const ebook = new DigitalProduct(
'Advanced JavaScript eBook',
19.99,
'Books',
2.5 * 1024 * 1024, // 2.5MB in bytes
'https://example.com/download/ebook'
);
inventory.addProduct(ebook);
// Test operations
console.log(inventory.getInventorySummary());
// Update product
laptop.name = 'Gaming Laptop';
laptop.price = 1299.99;
// Search
console.log(inventory.searchProducts('java'));
// Get low stock
console.log(inventory.getLowStockProducts(5));
// Digital product specific
console.log(ebook.download());
console.log(ebook.getProductInfo());
Advanced OOP Patterns
Factory Pattern:
javascript
class UserFactory {
static createUser(type, data) {
switch(type) {
case 'admin':
return new Admin(data);
case 'customer':
return new Customer(data);
case 'vendor':
return new Vendor(data);
default:
throw new Error(`Invalid user type: ${type}`);
}
}
}
class Admin {
constructor(data) {
this.username = data.username;
this.email = data.email;
this.permissions = data.permissions || ['all'];
this.role = 'admin';
}
manageUsers() {
return `${this.username} is managing users`;
}
}
class Customer {
constructor(data) {
this.username = data.username;
this.email = data.email;
this.shippingAddress = data.shippingAddress;
this.role = 'customer';
}
placeOrder() {
return `${this.username} placed an order`;
}
}
// Usage
const admin = UserFactory.createUser('admin', {
username: 'superadmin',
email: 'admin@example.com'
});
const customer = UserFactory.createUser('customer', {
username: 'john_doe',
email: 'john@example.com',
shippingAddress: '123 Main St'
});
console.log(admin.manageUsers());
console.log(customer.placeOrder());
Singleton Pattern:
javascript
class Logger {
static #instance = null;
#logs = [];
constructor() {
if (Logger.#instance) {
return Logger.#instance;
}
Logger.#instance = this;
}
log(message, level = 'INFO') {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message
};
this.#logs.push(logEntry);
console.log(`[${level}] ${logEntry.timestamp}: ${message}`);
// Keep only last 1000 logs
if (this.#logs.length > 1000) {
this.#logs.shift();
}
}
getLogs(level = null, limit = 100) {
let filtered = this.#logs;
if (level) {
filtered = filtered.filter(log => log.level === level);
}
return filtered.slice(-limit);
}
clearLogs() {
this.#logs = [];
this.log('Logs cleared', 'WARNING');
}
// Static method to get instance
static getInstance() {
if (!Logger.#instance) {
Logger.#instance = new Logger();
}
return Logger.#instance;
}
}
// Usage - always get the same instance
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true
logger1.log('Application started');
logger2.log('User logged in', 'INFO');
logger1.log('Database error', 'ERROR');
console.log(logger1.getLogs('ERROR'));
Strategy Pattern:
javascript
// Strategy interface
class PaymentStrategy {
pay(amount) {
throw new Error('pay() must be implemented');
}
}
// Concrete strategies
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, expiry, cvv) {
super();
this.cardNumber = cardNumber;
this.expiry = expiry;
this.cvv = cvv;
}
pay(amount) {
console.log(`Processing credit card payment of $${amount}`);
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
transactionId: `CC_${Date.now()}`,
amount,
method: 'credit_card'
});
}, 1000);
});
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Processing PayPal payment of $${amount} for ${this.email}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
transactionId: `PP_${Date.now()}`,
amount,
method: 'paypal'
});
}, 800);
});
}
}
class CryptoPayment extends PaymentStrategy {
constructor(walletAddress, currency = 'BTC') {
super();
this.walletAddress = walletAddress;
this.currency = currency;
}
pay(amount) {
console.log(`Processing ${this.currency} payment of $${amount}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
transactionId: `CRYPTO_${Date.now()}`,
amount,
method: 'crypto',
currency: this.currency
});
}, 1500);
});
}
}
// Context class
class PaymentProcessor {
constructor() {
this.strategy = null;
}
setStrategy(strategy) {
if (!(strategy instanceof PaymentStrategy)) {
throw new Error('Invalid payment strategy');
}
this.strategy = strategy;
console.log(`Payment method set to ${strategy.constructor.name}`);
}
async processPayment(amount) {
if (!this.strategy) {
throw new Error('No payment strategy set');
}
console.log(`Initiating payment of $${amount}...`);
try {
const result = await this.strategy.pay(amount);
console.log(`Payment successful: ${result.transactionId}`);
return result;
} catch (error) {
console.error('Payment failed:', error);
throw error;
}
}
}
// Usage
const processor = new PaymentProcessor();
// Use credit card
processor.setStrategy(new CreditCardPayment('4111111111111111', '12/25', '123'));
processor.processPayment(99.99).then(console.log);
// Switch to PayPal
setTimeout(() => {
processor.setStrategy(new PayPalPayment('user@example.com'));
processor.processPayment(49.99).then(console.log);
}, 2000);
Best Practices
1. Use Classes for Complex Objects
javascript
// GOOD: Use class for complex entity
class Order {
constructor(items, customer) {
this.items = items;
this.customer = customer;
this.status = 'pending';
this.createdAt = new Date();
}
calculateTotal() {
return this.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
}
}
// BAD: Plain object for complex entity
const order = {
items: [...],
customer: {...},
status: 'pending',
createdAt: new Date(),
calculateTotal: function() {
// This creates new function for each order!
}
};
2. Favor Composition Over Inheritance
javascript
// Instead of deep inheritance chains:
// Vehicle → Car → SportsCar → RacingCar
// Use composition:
class Engine {
constructor(type, horsepower) {
this.type = type;
this.horsepower = horsepower;
}
start() {
console.log('Engine started');
}
}
class Transmission {
constructor(type, gears) {
this.type = type;
this.gears = gears;
}
shift(gear) {
console.log(`Shifted to gear ${gear}`);
}
}
class Car {
constructor(make, model, engine, transmission) {
this.make = make;
this.model = model;
this.engine = engine;
this.transmission = transmission;
}
drive() {
this.engine.start();
this.transmission.shift(1);
console.log(`${this.make} ${this.model} is driving`);
}
}
// Usage
const v8Engine = new Engine('V8', 450);
const manualTransmission = new Transmission('manual', 6);
const sportsCar = new Car('Ford', 'Mustang', v8Engine, manualTransmission);
sportsCar.drive();
3. Use Private Fields for Internal State
javascript
class BankAccount {
#balance = 0; // Private field
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
getBalance() {
return this.#balance;
}
}
4. Static Methods for Utility Functions
javascript
class MathUtils {
static sum(numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
static average(numbers) {
return this.sum(numbers) / numbers.length;
}
static randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
// Use without creating instance
console.log(MathUtils.sum([1, 2, 3, 4, 5]));
console.log(MathUtils.randomInt(1, 100));
5. Use Getters and Setters for Computed Properties
javascript
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
get perimeter() {
return 2 * (this.width + this.height);
}
set aspectRatio(ratio) {
this.height = this.width / ratio;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50
console.log(rect.perimeter); // 30
rect.aspectRatio = 2; // Sets height to 5 (10/2)
Remember: Choose the right tool for the job. Use constructor functions for simple cases, classes for complex OOP, and always consider composition over deep inheritance hierarchies!
Comments (0)
No comments yet. Be the first to comment!