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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?