Learning Objectives

  • Create classes with constructor and methods
  • Use static methods and properties
  • Implement getters and setters
  • Apply inheritance with extends
  • Understand private fields and methods

Basic Class Syntax

class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

const user = new User('John', 30);
console.log(user.greet()); // "Hello, I'm John"
console.log(user.name);    // "John"
console.log(user.age);     // 30

Constructor

The constructor method is called when creating a new instance:

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
        this.createdAt = new Date();
        this.posts = [];
    }
}

const user = new User('John', 'john@example.com');
console.log(user.createdAt); // Current date
console.log(user.posts);     // []

Methods

Define methods directly in the class:

class Calculator {
    add(a, b) {
        return a + b;
    }
    
    subtract(a, b) {
        return a - b;
    }
    
    multiply(a, b) {
        return a * b;
    }
    
    divide(a, b) {
        if (b === 0) {
            throw new Error('Division by zero');
        }
        return a / b;
    }
}

const calc = new Calculator();
console.log(calc.add(5, 3));      // 8
console.log(calc.multiply(4, 5)); // 20

Static Methods

Called on the class itself, not instances:

class MathUtils {
    static add(a, b) {
        return a + b;
    }
    
    static max(...numbers) {
        return Math.max(...numbers);
    }
    
    static random(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
}

// Call on class, not instance
console.log(MathUtils.add(5, 3));        // 8
console.log(MathUtils.max(1, 5, 3, 9));  // 9
console.log(MathUtils.random(1, 10));    // Random number 1-10

// Cannot call on instance
const utils = new MathUtils();
// utils.add(5, 3); // TypeError!

Getters and Setters

class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    
    set fullName(name) {
        [this.firstName, this.lastName] = name.split(' ');
    }
    
    get initials() {
        return `${this.firstName[0]}${this.lastName[0]}`;
    }
}

const user = new User('John', 'Doe');
console.log(user.fullName); // "John Doe" (getter)
console.log(user.initials); // "JD"

user.fullName = 'Jane Smith'; // (setter)
console.log(user.firstName); // "Jane"
console.log(user.lastName);  // "Smith"

Inheritance with extends

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        return `${this.name} makes a sound`;
    }
    
    move() {
        return `${this.name} moves`;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Call parent constructor
        this.breed = breed;
    }
    
    speak() {
        return `${this.name} barks`;
    }
    
    fetch() {
        return `${this.name} fetches the ball`;
    }
}

const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.speak());  // "Rex barks" (overridden)
console.log(dog.move());   // "Rex moves" (inherited)
console.log(dog.fetch());  // "Rex fetches the ball"
console.log(dog.breed);    // "German Shepherd"

super Keyword

Call parent class methods:

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        return `${this.name} makes a sound`;
    }
}

class Cat extends Animal {
    speak() {
        const parentSpeak = super.speak();
        return `${parentSpeak} and meows`;
    }
}

const cat = new Cat('Whiskers');
console.log(cat.speak());
// "Whiskers makes a sound and meows"

Private Fields

Use # for private fields (ES2022):

class BankAccount {
    #balance = 0;
    #pin;
    
    constructor(initialBalance, pin) {
        this.#balance = initialBalance;
        this.#pin = pin;
    }
    
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            return true;
        }
        return false;
    }
    
    withdraw(amount, pin) {
        if (pin !== this.#pin) {
            throw new Error('Invalid PIN');
        }
        if (amount > this.#balance) {
            throw new Error('Insufficient funds');
        }
        this.#balance -= amount;
        return amount;
    }
    
    getBalance(pin) {
        if (pin !== this.#pin) {
            throw new Error('Invalid PIN');
        }
        return this.#balance;
    }
}

const account = new BankAccount(1000, '1234');
account.deposit(500);
console.log(account.getBalance('1234')); // 1500
// console.log(account.#balance); // SyntaxError!

Private Methods

class User {
    #password;
    
    constructor(name, password) {
        this.name = name;
        this.#password = this.#hashPassword(password);
    }
    
    #hashPassword(password) {
        // Private method - only accessible inside class
        return btoa(password); // Simple encoding for demo
    }
    
    verifyPassword(password) {
        return this.#hashPassword(password) === this.#password;
    }
    
    changePassword(oldPassword, newPassword) {
        if (!this.verifyPassword(oldPassword)) {
            throw new Error('Invalid password');
        }
        this.#password = this.#hashPassword(newPassword);
    }
}

const user = new User('John', 'secret123');
console.log(user.verifyPassword('secret123')); // true
console.log(user.verifyPassword('wrong'));     // false
// user.#hashPassword('test'); // SyntaxError!

Static Properties

class Config {
    static apiUrl = 'https://api.example.com';
    static timeout = 5000;
    static version = '1.0.0';
    
    static getConfig() {
        return {
            apiUrl: this.apiUrl,
            timeout: this.timeout,
            version: this.version
        };
    }
}

console.log(Config.apiUrl);      // "https://api.example.com"
console.log(Config.getConfig()); // { apiUrl: ..., timeout: ..., version: ... }

Real-World Examples

User Management

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
        this.createdAt = new Date();
        this.id = User.generateId();
    }
    
    static #nextId = 1;
    
    static generateId() {
        return this.#nextId++;
    }
    
    static fromJSON(json) {
        return new User(json.name, json.email);
    }
    
    toJSON() {
        return {
            id: this.id,
            name: this.name,
            email: this.email,
            createdAt: this.createdAt
        };
    }
}

const user1 = new User('John', 'john@example.com');
const user2 = User.fromJSON({ name: 'Jane', email: 'jane@example.com' });
console.log(user1.toJSON());

Todo List

class TodoList {
    #todos = [];
    
    add(text) {
        this.#todos.push({
            id: Date.now(),
            text,
            done: false,
            createdAt: new Date()
        });
    }
    
    toggle(id) {
        const todo = this.#todos.find(t => t.id === id);
        if (todo) {
            todo.done = !todo.done;
        }
    }
    
    remove(id) {
        this.#todos = this.#todos.filter(t => t.id !== id);
    }
    
    get completed() {
        return this.#todos.filter(t => t.done);
    }
    
    get pending() {
        return this.#todos.filter(t => !t.done);
    }
    
    get all() {
        return [...this.#todos];
    }
}

const todos = new TodoList();
todos.add('Learn JavaScript');
todos.add('Build app');
console.log(todos.pending.length); // 2

API Client

class ApiClient {
    constructor(baseUrl, options = {}) {
        this.baseUrl = baseUrl;
        this.timeout = options.timeout || 5000;
        this.headers = options.headers || {};
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseUrl}${endpoint}`;
        const config = {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...this.headers,
                ...options.headers
            }
        };
        
        const response = await fetch(url, config);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        return response.json();
    }
    
    async get(endpoint) {
        return this.request(endpoint);
    }
    
    async post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }
    
    async put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }
    
    async delete(endpoint) {
        return this.request(endpoint, {
            method: 'DELETE'
        });
    }
}

const api = new ApiClient('https://api.example.com');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });

Class vs Function Constructor

Old Way (Function Constructor)

function User(name, age) {
    this.name = name;
    this.age = age;
}

User.prototype.greet = function() {
    return `Hello, I'm ${this.name}`;
};

User.createGuest = function() {
    return new User('Guest', 0);
};

const user = new User('John', 30);

Modern Way (Class)

class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
    
    static createGuest() {
        return new User('Guest', 0);
    }
}

const user = new User('John', 30);

Classes are cleaner and more intuitive!

Best Practices

1. Use PascalCase for class names
class UserAccount { }  // Good
class userAccount { }  // Bad
2. Initialize properties in constructor
class User {
    constructor(name) {
        this.name = name;
        this.posts = [];     // Initialize
        this.followers = []; // Initialize
    }
}
3. Use static methods for utilities
class DateUtils {
    static format(date) {
        return date.toISOString();
    }
    
    static parse(str) {
        return new Date(str);
    }
}
4. Use private fields for encapsulation
class Counter {
    #count = 0;
    
    increment() {
        this.#count++;
    }
    
    get value() {
        return this.#count;
    }
}
5. Call super() first in child constructor
class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Must be first!
        this.breed = breed;
    }
}

Key Takeaways