JavaScript Classes and OOP (Object-Oriented Programming)

1. Class Declaration and Constructor Methods

Class Declaration Syntax

Feature Syntax Description
Basic Class class Name {} Class declaration (not hoisted)
Constructor constructor(params) {} Initialize instance (only one per class)
Instance Method method() {} Method on prototype
Static Method static method() {} Method on class itself
Field Declaration field = value; Public instance field
Static Field static field = value; Static field on class
Private Field #field = value; Private instance field
Getter get prop() {} Property getter accessor
Setter set prop(val) {} Property setter accessor

Constructor Features

Feature Description Behavior
Return Value Implicit return of this Explicit object return overrides instance
Super Call Must call super() in derived class Before accessing this in derived constructor
Parameters Support all parameter types Default, rest, destructuring parameters
Omitting Constructor Default constructor created Base: constructor() {}; Derived: super(...args)
Multiple Constructors Not supported Use factory methods or optional parameters

Example: Basic class declaration

// Simple class
class Person {
    // Constructor
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Instance method
    greet() {
        return `Hello, I'm ${this.name}`;
    }
    
    // Instance method
    getAge() {
        return this.age;
    }
}

// Create instances
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);

console.log(alice.greet());     // "Hello, I'm Alice"
console.log(alice.getAge());    // 30
console.log(bob.greet());       // "Hello, I'm Bob"

// Methods are on prototype
console.log(alice.greet === bob.greet);  // true (shared method)

// instanceof check
console.log(alice instanceof Person);    // true

// Class is not hoisted
// const p = new MyClass();  // ReferenceError
// class MyClass {}

// Classes are strict mode by default
class StrictClass {
    constructor() {
        // Implicit strict mode
        // x = 10;  // ReferenceError (no implicit globals)
    }
}

Example: Constructor with field declarations

// Class with field declarations (ES2022)
class User {
    // Public fields (initialized on instance)
    id = 0;
    name = 'Anonymous';
    role = 'user';
    createdAt = Date.now();
    
    // Constructor overrides field defaults
    constructor(id, name, role) {
        this.id = id;
        this.name = name;
        if (role) this.role = role;
    }
    
    getInfo() {
        return `${this.name} (${this.role})`;
    }
}

const user1 = new User(1, 'Alice', 'admin');
console.log(user1.getInfo());        // "Alice (admin)"
console.log(user1.createdAt);        // timestamp

const user2 = new User(2, 'Bob');
console.log(user2.role);             // "user" (default)

// Fields are instance properties (not on prototype)
console.log(user1.hasOwnProperty('name'));  // true
console.log('name' in User.prototype);      // false

// Methods are on prototype
console.log(user1.hasOwnProperty('getInfo'));  // false
console.log('getInfo' in User.prototype);      // true

Example: Constructor patterns and validation

// Constructor with validation
class Rectangle {
    constructor(width, height) {
        if (width <= 0 || height <= 0) {
            throw new Error('Dimensions must be positive');
        }
        this.width = width;
        this.height = height;
    }
    
    area() {
        return this.width * this.height;
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area());  // 50

// const invalid = new Rectangle(-5, 10);  // Error

// Constructor with default parameters
class Point {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
    
    distance(other) {
        const dx = this.x - other.x;
        const dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

const p1 = new Point(3, 4);
const p2 = new Point();  // (0, 0)
console.log(p1.distance(p2));  // 5

// Constructor with destructuring
class Config {
    constructor({host, port = 3000, secure = false} = {}) {
        this.host = host || 'localhost';
        this.port = port;
        this.secure = secure;
    }
    
    getUrl() {
        const protocol = this.secure ? 'https' : 'http';
        return `${protocol}://${this.host}:${this.port}`;
    }
}

const config1 = new Config({host: 'example.com', secure: true});
console.log(config1.getUrl());  // "https://example.com:3000"

const config2 = new Config();
console.log(config2.getUrl());  // "http://localhost:3000"

// Constructor with rest parameters
class Team {
    constructor(name, ...members) {
        this.name = name;
        this.members = members;
    }
    
    addMember(member) {
        this.members.push(member);
    }
}

const team = new Team('Development', 'Alice', 'Bob', 'Charlie');
console.log(team.members);  // ['Alice', 'Bob', 'Charlie']

Example: Constructor return behavior

// Normal constructor - returns instance
class Normal {
    constructor(value) {
        this.value = value;
    }
}

const n = new Normal(5);
console.log(n instanceof Normal);  // true
console.log(n.value);              // 5

// Constructor returning object - overrides instance
class Custom {
    constructor(value) {
        this.value = value;
        
        // Returning object overrides the instance
        return {
            value: value * 2,
            custom: true
        };
    }
}

const c = new Custom(5);
console.log(c instanceof Custom);  // false (returned object)
console.log(c.value);              // 10
console.log(c.custom);             // true

// Returning primitive - ignored
class Primitive {
    constructor(value) {
        this.value = value;
        return 42;  // Ignored
    }
}

const p = new Primitive(5);
console.log(p instanceof Primitive);  // true
console.log(p.value);                 // 5

// Singleton pattern with constructor
class Singleton {
    static #instance;
    
    constructor() {
        if (Singleton.#instance) {
            return Singleton.#instance;
        }
        
        this.createdAt = Date.now();
        Singleton.#instance = this;
    }
}

const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2);  // true (same instance)
Key Points: Classes are not hoisted (temporal dead zone). Constructor runs when new is called. Only one constructor per class. Fields are instance properties; methods are prototype properties. Always in strict mode. Can return object from constructor to override instance.

2. Instance Methods and Static Methods

Method Types Comparison

Method Type Syntax Location Access via this refers to
Instance Method method() {} Class.prototype instance.method() Instance object
Static Method static method() {} Class itself Class.method() Class constructor
Private Instance #method() {} Class (hidden) this.#method() (internal) Instance object
Private Static static #method() {} Class (hidden) Class.#method() (internal) Class constructor

Method Features

Feature Instance Methods Static Methods
Access to this Instance properties and methods Static properties and methods
Inheritance Inherited by subclasses Inherited by subclasses
Shared Shared among all instances One copy on class
Use Case Instance-specific behavior Utility functions, factories, helpers
Call without new ❌ Needs instance ✓ Can call directly

Example: Instance methods

class Calculator {
    constructor(value = 0) {
        this.value = value;
    }
    
    // Instance methods - operate on instance data
    add(n) {
        this.value += n;
        return this;  // Method chaining
    }
    
    subtract(n) {
        this.value -= n;
        return this;
    }
    
    multiply(n) {
        this.value *= n;
        return this;
    }
    
    divide(n) {
        if (n === 0) throw new Error('Division by zero');
        this.value /= n;
        return this;
    }
    
    getValue() {
        return this.value;
    }
    
    reset() {
        this.value = 0;
        return this;
    }
}

const calc = new Calculator(10);
const result = calc.add(5).multiply(2).subtract(10).getValue();
console.log(result);  // 20

// Methods are on prototype
console.log(calc.hasOwnProperty('add'));        // false
console.log('add' in Calculator.prototype);     // true

// Shared across instances
const calc2 = new Calculator();
console.log(calc.add === calc2.add);  // true (same function)

Example: Static methods

class MathUtils {
    // Static methods - utility functions
    static add(a, b) {
        return a + b;
    }
    
    static multiply(a, b) {
        return a * b;
    }
    
    static max(...numbers) {
        return Math.max(...numbers);
    }
    
    static clamp(value, min, max) {
        return Math.min(Math.max(value, min), max);
    }
    
    // Static method accessing other static methods
    static average(...numbers) {
        const sum = numbers.reduce((a, b) => this.add(a, b), 0);
        return sum / numbers.length;
    }
}

// Call static methods on class (not instances)
console.log(MathUtils.add(5, 3));           // 8
console.log(MathUtils.max(1, 5, 3, 9, 2));  // 9
console.log(MathUtils.clamp(15, 0, 10));    // 10
console.log(MathUtils.average(1, 2, 3, 4)); // 2.5

// Static methods are on class, not prototype
console.log('add' in MathUtils);            // true
console.log('add' in MathUtils.prototype);  // false

// Can't call on instances
const util = new MathUtils();
// util.add(1, 2);  // TypeError: util.add is not a function

// Factory pattern with static methods
class User {
    constructor(name, email, role) {
        this.name = name;
        this.email = email;
        this.role = role;
    }
    
    // Static factory methods
    static createAdmin(name, email) {
        return new User(name, email, 'admin');
    }
    
    static createGuest(name) {
        return new User(name, `${name}@guest.local`, 'guest');
    }
    
    static fromJSON(json) {
        const data = JSON.parse(json);
        return new User(data.name, data.email, data.role);
    }
}

const admin = User.createAdmin('Alice', 'alice@example.com');
const guest = User.createGuest('Bob');
const parsed = User.fromJSON('{"name":"Charlie","email":"c@e.com","role":"user"}');

console.log(admin.role);   // "admin"
console.log(guest.email);  // "bob@guest.local"

Example: Mixing instance and static methods

class Counter {
    // Static field for counting all instances
    static totalInstances = 0;
    static #allCounters = [];
    
    // Instance field
    value = 0;
    
    constructor(initialValue = 0) {
        this.value = initialValue;
        Counter.totalInstances++;
        Counter.#allCounters.push(this);
    }
    
    // Instance methods
    increment() {
        this.value++;
        return this;
    }
    
    decrement() {
        this.value--;
        return this;
    }
    
    getValue() {
        return this.value;
    }
    
    // Static methods
    static getTotalInstances() {
        return Counter.totalInstances;
    }
    
    static getSumOfAll() {
        return Counter.#allCounters.reduce((sum, counter) => sum + counter.value, 0);
    }
    
    static resetAll() {
        Counter.#allCounters.forEach(counter => counter.value = 0);
    }
    
    static getMaxValue() {
        if (Counter.#allCounters.length === 0) return null;
        return Math.max(...Counter.#allCounters.map(c => c.value));
    }
}

const c1 = new Counter(5);
const c2 = new Counter(10);
const c3 = new Counter(3);

console.log(Counter.getTotalInstances());  // 3
console.log(Counter.getSumOfAll());        // 18

c1.increment().increment();
console.log(Counter.getSumOfAll());        // 20
console.log(Counter.getMaxValue());        // 10

Counter.resetAll();
console.log(c1.getValue());                // 0
console.log(c2.getValue());                // 0

Example: Static method inheritance

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    // Instance method
    speak() {
        return `${this.name} makes a sound`;
    }
    
    // Static method
    static info() {
        return 'Animals are living organisms';
    }
    
    static compare(a, b) {
        return a.name.localeCompare(b.name);
    }
}

class Dog extends Animal {
    // Override instance method
    speak() {
        return `${this.name} barks`;
    }
    
    // Static method in derived class
    static info() {
        return super.info() + ', and dogs are mammals';
    }
}

// Static methods are inherited
console.log(Dog.info());  // "Animals are living organisms, and dogs are mammals"

const dog1 = new Dog('Max');
const dog2 = new Dog('Buddy');

// Inherited static method works with derived class instances
console.log(Dog.compare(dog1, dog2));  // Compares names

// Instance methods
console.log(dog1.speak());  // "Max barks"
Best Practices: Use instance methods for behavior specific to each object. Use static methods for utilities, factories, and class-level operations. Static methods can't access instance properties (no this referring to instance). Both instance and static methods are inherited. Use static for constants, configuration, and helper functions.

3. Class Inheritance (extends) and super Keyword

Inheritance Syntax

Syntax Description Usage
extends class Child extends Parent Create subclass inheriting from parent
super() super(args) Call parent constructor (required in child constructor)
super.method() super.methodName(args) Call parent's instance method
super.property super.prop Access parent's property or method
Static super super.staticMethod() Call parent's static method (in static context)

super Rules

Rule Description Example/Note
Constructor super Must call super() before accessing this ReferenceError if accessing this before super()
No constructor Default constructor calls super(...args) Automatically generated if omitted
Return before super Can return object without calling super() Only if returning object explicitly
Method context super only in class methods Not in arrow functions or regular functions
Static super In static method, super refers to parent class Access static methods/properties

Example: Basic inheritance

// Parent class
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    speak() {
        return `${this.name} makes a sound`;
    }
    
    getAge() {
        return this.age;
    }
}

// Child class
class Dog extends Animal {
    constructor(name, age, breed) {
        // Must call super() before accessing this
        super(name, age);  // Call parent constructor
        this.breed = breed;
    }
    
    // Override parent method
    speak() {
        return `${this.name} barks`;
    }
    
    // New method specific to Dog
    fetch() {
        return `${this.name} fetches the ball`;
    }
}

const dog = new Dog('Max', 3, 'Labrador');
console.log(dog.name);      // "Max" (from parent)
console.log(dog.breed);     // "Labrador" (from child)
console.log(dog.speak());   // "Max barks" (overridden)
console.log(dog.getAge());  // 3 (inherited)
console.log(dog.fetch());   // "Max fetches the ball" (child method)

// Prototype chain
console.log(dog instanceof Dog);      // true
console.log(dog instanceof Animal);   // true
console.log(dog instanceof Object);   // true

// Inheritance chain
console.log(Object.getPrototypeOf(dog) === Dog.prototype);  // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);  // true

Example: super in methods

class Shape {
    constructor(color) {
        this.color = color;
    }
    
    describe() {
        return `A ${this.color} shape`;
    }
    
    getColor() {
        return this.color;
    }
}

class Circle extends Shape {
    constructor(color, radius) {
        super(color);
        this.radius = radius;
    }
    
    // Call parent method and extend
    describe() {
        const base = super.describe();  // Call parent's describe()
        return `${base} with radius ${this.radius}`;
    }
    
    area() {
        return Math.PI * this.radius ** 2;
    }
}

const circle = new Circle('red', 5);
console.log(circle.describe());  // "A red shape with radius 5"
console.log(circle.getColor());  // "red" (inherited)
console.log(circle.area());      // 78.54...

// More complex example
class Rectangle extends Shape {
    constructor(color, width, height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    describe() {
        // Extend parent's description
        return super.describe() + ` (${this.width}x${this.height})`;
    }
    
    area() {
        return this.width * this.height;
    }
    
    // Method using both parent and child properties
    getFullDescription() {
        return `${super.describe()}, area: ${this.area()}`;
    }
}

const rect = new Rectangle('blue', 10, 5);
console.log(rect.describe());           // "A blue shape (10x5)"
console.log(rect.getFullDescription()); // "A blue shape, area: 50"

Example: Multi-level inheritance

// Three levels of inheritance
class LivingBeing {
    constructor(name) {
        this.name = name;
        this.alive = true;
    }
    
    breathe() {
        return `${this.name} is breathing`;
    }
}

class Animal2 extends LivingBeing {
    constructor(name, species) {
        super(name);
        this.species = species;
    }
    
    move() {
        return `${this.name} is moving`;
    }
    
    identify() {
        return `${this.name} is a ${this.species}`;
    }
}

class Dog2 extends Animal2 {
    constructor(name, breed) {
        super(name, 'dog');  // Species is always 'dog'
        this.breed = breed;
    }
    
    bark() {
        return `${this.name} barks`;
    }
    
    // Override with super call
    identify() {
        return super.identify() + ` (${this.breed})`;
    }
}

const dog2 = new Dog2('Rex', 'German Shepherd');
console.log(dog2.breathe());   // "Rex is breathing" (from LivingBeing)
console.log(dog2.move());      // "Rex is moving" (from Animal2)
console.log(dog2.bark());      // "Rex barks" (from Dog2)
console.log(dog2.identify());  // "Rex is a dog (German Shepherd)"

// Prototype chain
console.log(dog2 instanceof Dog2);         // true
console.log(dog2 instanceof Animal2);      // true
console.log(dog2 instanceof LivingBeing);  // true

Example: Static method inheritance

class BaseModel {
    static tableName = 'base';
    
    constructor(data) {
        this.data = data;
    }
    
    // Static method
    static findAll() {
        console.log(`Finding all from ${this.tableName}`);
        return [];
    }
    
    static findById(id) {
        console.log(`Finding ${id} from ${this.tableName}`);
        return null;
    }
    
    // Static method calling other static methods
    static findOrCreate(id) {
        let record = this.findById(id);
        if (!record) {
            console.log(`Creating new record in ${this.tableName}`);
            record = new this({id});
        }
        return record;
    }
}

class User2 extends BaseModel {
    static tableName = 'users';
    
    // Override static method
    static findById(id) {
        console.log(`[User] Finding user ${id}`);
        return super.findById(id);  // Can call parent's static method
    }
    
    // New static method
    static findByEmail(email) {
        console.log(`Finding user with email ${email} from ${this.tableName}`);
        return null;
    }
}

// Static methods are inherited
User2.findAll();         // "Finding all from users"
User2.findById(1);       // "[User] Finding user 1" then "Finding 1 from users"
User2.findByEmail('a@b.com');  // "Finding user with email a@b.com from users"

// Static method using this correctly
const user = User2.findOrCreate(5);
console.log(user instanceof User2);  // true
Important: super() must be called before accessing this in derived constructor. super in methods refers to parent's prototype. super in static methods refers to parent class. Method overriding creates new method; use super.method() to call parent version. Prototype chain: Child → Parent → ... → Object.

4. Private Fields (#field) and Private Methods (#method)

Private Member Types

Type Syntax Access Inheritance
Private Field #field = value Only within class body Not inherited
Private Method #method() {} Only within class body Not inherited
Private Getter get #prop() {} Only within class body Not inherited
Private Setter set #prop(v) {} Only within class body Not inherited
Private Static Field static #field = value Only within class (static context) Not inherited
Private Static Method static #method() {} Only within class (static context) Not inherited

Private vs Public Comparison

Feature Public Private (#)
Access Outside Class ✓ Yes ✗ No (SyntaxError)
Access via this this.field this.#field
Inheritance ✓ Inherited ✗ Not inherited
Bracket Notation ✓ obj['field'] ✗ Not accessible
Reflection ✓ Object.keys, etc. ✗ Not enumerable
Name Collision Can collide with child Each class has separate namespace

Example: Private fields and methods

class BankAccount {
    // Private fields (must be declared)
    #balance = 0;
    #accountNumber;
    #transactions = [];
    
    // Public field
    accountType = 'checking';
    
    constructor(accountNumber, initialBalance) {
        this.#accountNumber = accountNumber;
        this.#balance = initialBalance;
        this.#logTransaction('Initial deposit', initialBalance);
    }
    
    // Private method
    #logTransaction(type, amount) {
        this.#transactions.push({
            type,
            amount,
            date: new Date(),
            balance: this.#balance
        });
    }
    
    // Private validation method
    #validateAmount(amount) {
        if (typeof amount !== 'number' || amount <= 0) {
            throw new Error('Invalid amount');
        }
    }
    
    // Public methods using private fields/methods
    deposit(amount) {
        this.#validateAmount(amount);
        this.#balance += amount;
        this.#logTransaction('Deposit', amount);
        return this.#balance;
    }
    
    withdraw(amount) {
        this.#validateAmount(amount);
        
        if (amount > this.#balance) {
            throw new Error('Insufficient funds');
        }
        
        this.#balance -= amount;
        this.#logTransaction('Withdrawal', -amount);
        return this.#balance;
    }
    
    getBalance() {
        return this.#balance;
    }
    
    getTransactionHistory() {
        // Return copy to prevent external modification
        return [...this.#transactions];
    }
    
    // Private getter
    get #formattedBalance() {
        return `${this.#balance.toFixed(2)}`;
    }
    
    toString() {
        return `Account ${this.#accountNumber}: ${this.#formattedBalance}`;
    }
}

const account = new BankAccount('1234567890', 1000);
console.log(account.getBalance());  // 1000

account.deposit(500);
console.log(account.getBalance());  // 1500

account.withdraw(200);
console.log(account.getBalance());  // 1300

// Private fields are truly private
console.log(account.#balance);       // SyntaxError
console.log(account['#balance']);    // undefined
console.log(account.balance);        // undefined

// Private methods not accessible
// account.#logTransaction('test', 100);  // SyntaxError

console.log(account.toString());  // "Account 1234567890: $1300.00"

Example: Private static members

class IDGenerator {
    // Private static field
    static #currentId = 0;
    static #usedIds = new Set();
    
    // Public instance field
    id;
    
    constructor() {
        this.id = IDGenerator.#generateId();
    }
    
    // Private static method
    static #generateId() {
        let newId;
        do {
            newId = ++IDGenerator.#currentId;
        } while (IDGenerator.#usedIds.has(newId));
        
        IDGenerator.#usedIds.add(newId);
        return newId;
    }
    
    // Public static method
    static releaseId(id) {
        IDGenerator.#usedIds.delete(id);
    }
    
    // Public static method accessing private static
    static getTotalGenerated() {
        return IDGenerator.#currentId;
    }
    
    static getActiveCount() {
        return IDGenerator.#usedIds.size;
    }
}

const obj1 = new IDGenerator();
const obj2 = new IDGenerator();
const obj3 = new IDGenerator();

console.log(obj1.id);  // 1
console.log(obj2.id);  // 2
console.log(obj3.id);  // 3

console.log(IDGenerator.getTotalGenerated());  // 3
console.log(IDGenerator.getActiveCount());     // 3

IDGenerator.releaseId(2);
console.log(IDGenerator.getActiveCount());     // 2

// Private static members not accessible
// console.log(IDGenerator.#currentId);  // SyntaxError

Example: Private fields with inheritance

class Base {
    #privateBase = 'base private';
    publicBase = 'base public';
    
    getPrivateBase() {
        return this.#privateBase;
    }
}

class Derived extends Base {
    #privateDerived = 'derived private';
    publicDerived = 'derived public';
    
    // Can have same name as parent's private field (separate namespace)
    #privateBase = 'derived has its own #privateBase';
    
    getPrivateDerived() {
        return this.#privateDerived;
    }
    
    getDerivedPrivateBase() {
        return this.#privateBase;  // Accesses Derived's #privateBase
    }
    
    getAllInfo() {
        return {
            // Can access parent's private via parent's public method
            parentPrivate: this.getPrivateBase(),  // "base private"
            // Own private fields
            derivedPrivate: this.#privateDerived,
            derivedPrivateBase: this.#privateBase,
            // Public fields
            publicBase: this.publicBase,
            publicDerived: this.publicDerived
        };
    }
}

const derived = new Derived();
console.log(derived.getAllInfo());
// {
//   parentPrivate: "base private",
//   derivedPrivate: "derived private",
//   derivedPrivateBase: "derived has its own #privateBase",
//   publicBase: "base public",
//   publicDerived: "derived public"
// }

// Each class has separate private namespace
console.log(derived.getPrivateBase());        // "base private"
console.log(derived.getDerivedPrivateBase()); // "derived has its own #privateBase"

Example: Practical private member use case

class SecureCache {
    // Private storage
    #cache = new Map();
    #accessLog = [];
    #maxSize = 100;
    
    // Private method for logging
    #log(operation, key) {
        this.#accessLog.push({
            operation,
            key,
            timestamp: Date.now()
        });
        
        // Keep log size manageable
        if (this.#accessLog.length > 1000) {
            this.#accessLog = this.#accessLog.slice(-500);
        }
    }
    
    // Private method for size check
    #checkSize() {
        if (this.#cache.size >= this.#maxSize) {
            // Remove oldest entry (first key)
            const firstKey = this.#cache.keys().next().value;
            this.#cache.delete(firstKey);
        }
    }
    
    // Public API
    set(key, value) {
        this.#checkSize();
        this.#cache.set(key, value);
        this.#log('set', key);
    }
    
    get(key) {
        this.#log('get', key);
        return this.#cache.get(key);
    }
    
    has(key) {
        return this.#cache.has(key);
    }
    
    delete(key) {
        this.#log('delete', key);
        return this.#cache.delete(key);
    }
    
    clear() {
        this.#cache.clear();
        this.#log('clear', null);
    }
    
    // Private getter
    get #stats() {
        return {
            size: this.#cache.size,
            maxSize: this.#maxSize,
            accessCount: this.#accessLog.length
        };
    }
    
    getStats() {
        return {...this.#stats};  // Return copy
    }
    
    getRecentAccess(count = 10) {
        return this.#accessLog.slice(-count);
    }
}

const cache = new SecureCache();
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.get('key1');

console.log(cache.getStats());
// {size: 2, maxSize: 100, accessCount: 3}

console.log(cache.getRecentAccess(5));
// Recent access log

// Internal state completely private
// cache.#cache  // SyntaxError
// cache.#log()  // SyntaxError
Key Points: Private members use # prefix and must be declared in class body. Truly private (not accessible outside class body). Not inherited by subclasses. Each class has separate private namespace (can have same names). Use for encapsulation, internal implementation details, sensitive data. Check existence with #field in obj (inside class only).

5. Getters (get) and Setters (set) in Classes

Accessor Types

Accessor Syntax Purpose Usage
Getter get propName() {} Read computed property obj.propName
Setter set propName(value) {} Write with validation obj.propName = value
Private Getter get #propName() {} Private computed property this.#propName
Private Setter set #propName(v) {} Private validated write this.#propName = v
Static Getter static get propName() {} Class-level computed property Class.propName
Static Setter static set propName(v) {} Class-level validated write Class.propName = v

Getter/Setter Features

Feature Description Behavior
Access Syntax Use like property (no parentheses) obj.prop not obj.prop()
Computed Values Calculate value on access No storage, derived from other properties
Validation Validate before setting Throw error or coerce value
Lazy Evaluation Compute only when accessed Can cache result
Side Effects Can trigger actions on get/set Logging, updates, notifications
Read-only Getter without setter Assignment ignored in non-strict, error in strict
Write-only Setter without getter Reading returns undefined

Example: Basic getters and setters

class Person {
    constructor(firstName, lastName, birthYear) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.birthYear = birthYear;
    }
    
    // Getter - computed property
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    
    // Setter - split and assign
    set fullName(name) {
        const [first, last] = name.split(' ');
        this.firstName = first;
        this.lastName = last;
    }
    
    // Getter - computed from other property
    get age() {
        return new Date().getFullYear() - this.birthYear;
    }
    
    // Read-only (getter without setter)
    get initials() {
        return `${this.firstName[0]}.${this.lastName[0]}.`;
    }
}

const person = new Person('John', 'Doe', 1990);

// Access like properties (no parentheses)
console.log(person.fullName);    // "John Doe"
console.log(person.age);         // 35 (computed)
console.log(person.initials);    // "J.D."

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

// Read-only property
person.initials = 'X.Y.';        // Ignored (non-strict) or error (strict)
console.log(person.initials);    // Still "J.S."

// Age is computed each time
console.log(person.age);         // Computed from current year

Example: Validation with setters

class Product {
    #price = 0;
    #quantity = 0;
    
    constructor(name) {
        this.name = name;
    }
    
    // Getter returns private field
    get price() {
        return this.#price;
    }
    
    // Setter with validation
    set price(value) {
        if (typeof value !== 'number') {
            throw new TypeError('Price must be a number');
        }
        if (value < 0) {
            throw new RangeError('Price cannot be negative');
        }
        this.#price = value;
    }
    
    get quantity() {
        return this.#quantity;
    }
    
    set quantity(value) {
        if (!Number.isInteger(value)) {
            throw new TypeError('Quantity must be an integer');
        }
        if (value < 0) {
            throw new RangeError('Quantity cannot be negative');
        }
        this.#quantity = value;
    }
    
    // Computed property
    get total() {
        return this.#price * this.#quantity;
    }
    
    // Computed with formatting
    get formattedTotal() {
        return `${this.total.toFixed(2)}`;
    }
}

const product = new Product('Widget');

product.price = 19.99;
product.quantity = 5;

console.log(product.price);          // 19.99
console.log(product.quantity);       // 5
console.log(product.total);          // 99.95
console.log(product.formattedTotal); // "$99.95"

// Validation in action
try {
    product.price = -10;  // RangeError: Price cannot be negative
} catch (e) {
    console.error(e.message);
}

try {
    product.quantity = 3.5;  // TypeError: Quantity must be an integer
} catch (e) {
    console.error(e.message);
}

Example: Lazy evaluation and caching

class DataProcessor {
    #data;
    #processedCache = null;
    #isDirty = true;
    
    constructor(data) {
        this.#data = data;
    }
    
    // Setter invalidates cache
    set data(newData) {
        this.#data = newData;
        this.#isDirty = true;  // Mark cache as invalid
        this.#processedCache = null;
    }
    
    get data() {
        return this.#data;
    }
    
    // Lazy evaluation with caching
    get processed() {
        if (this.#isDirty || this.#processedCache === null) {
            console.log('Computing processed data...');
            // Expensive operation
            this.#processedCache = this.#data.map(x => x * 2);
            this.#isDirty = false;
        } else {
            console.log('Using cached data');
        }
        return this.#processedCache;
    }
    
    // Read-only computed statistics
    get stats() {
        const data = this.processed;
        return {
            count: data.length,
            sum: data.reduce((a, b) => a + b, 0),
            average: data.reduce((a, b) => a + b, 0) / data.length,
            min: Math.min(...data),
            max: Math.max(...data)
        };
    }
}

const processor = new DataProcessor([1, 2, 3, 4, 5]);

console.log(processor.processed);  // Logs: "Computing...", returns [2, 4, 6, 8, 10]
console.log(processor.processed);  // Logs: "Using cached data", returns same

console.log(processor.stats);      // Computes statistics

processor.data = [10, 20, 30];     // Invalidates cache
console.log(processor.processed);  // Logs: "Computing...", returns [20, 40, 60]

Example: Static getters and setters

class Configuration {
    static #config = {
        apiUrl: 'https://api.example.com',
        timeout: 5000,
        debug: false
    };
    
    // Static getter
    static get apiUrl() {
        return Configuration.#config.apiUrl;
    }
    
    // Static setter with validation
    static set apiUrl(url) {
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            throw new Error('URL must start with http:// or https://');
        }
        Configuration.#config.apiUrl = url;
    }
    
    static get timeout() {
        return Configuration.#config.timeout;
    }
    
    static set timeout(ms) {
        if (ms < 0) throw new Error('Timeout cannot be negative');
        Configuration.#config.timeout = ms;
    }
    
    static get debug() {
        return Configuration.#config.debug;
    }
    
    static set debug(value) {
        Configuration.#config.debug = Boolean(value);
    }
    
    // Read-only computed configuration
    static get summary() {
        return `API: ${Configuration.apiUrl}, Timeout: ${Configuration.timeout}ms`;
    }
    
    // Method to get all config (snapshot)
    static getAll() {
        return {...Configuration.#config};
    }
}

// Access static getters/setters on class
console.log(Configuration.apiUrl);     // "https://api.example.com"
console.log(Configuration.timeout);    // 5000

Configuration.apiUrl = 'https://new-api.com';
Configuration.timeout = 10000;
Configuration.debug = true;

console.log(Configuration.summary);
// "API: https://new-api.com, Timeout: 10000ms"

console.log(Configuration.getAll());
// {apiUrl: "https://new-api.com", timeout: 10000, debug: true}

Example: Private getters and setters

class Temperature {
    #celsius = 0;
    
    constructor(celsius) {
        this.celsius = celsius;  // Use public setter
    }
    
    // Public getter/setter for Celsius
    get celsius() {
        return this.#celsius;
    }
    
    set celsius(value) {
        if (value < -273.15) {
            throw new Error('Below absolute zero');
        }
        this.#celsius = value;
    }
    
    // Public getter/setter for Fahrenheit
    get fahrenheit() {
        return this.#celsius * 9/5 + 32;
    }
    
    set fahrenheit(value) {
        this.celsius = (value - 32) * 5/9;  // Use celsius setter
    }
    
    // Private getter - internal formatting
    get #formatted() {
        return {
            c: `${this.#celsius.toFixed(1)}°C`,
            f: `${this.fahrenheit.toFixed(1)}°F`
        };
    }
    
    // Public method using private getter
    toString() {
        const fmt = this.#formatted;
        return `${fmt.c} (${fmt.f})`;
    }
    
    // Computed properties
    get kelvin() {
        return this.#celsius + 273.15;
    }
    
    get description() {
        if (this.#celsius < 0) return 'Freezing';
        if (this.#celsius < 10) return 'Cold';
        if (this.#celsius < 20) return 'Cool';
        if (this.#celsius < 30) return 'Warm';
        return 'Hot';
    }
}

const temp = new Temperature(25);
console.log(temp.celsius);      // 25
console.log(temp.fahrenheit);   // 77
console.log(temp.kelvin);       // 298.15
console.log(temp.description);  // "Warm"
console.log(temp.toString());   // "25.0°C (77.0°F)"

temp.fahrenheit = 32;  // Set via Fahrenheit
console.log(temp.celsius);      // 0
console.log(temp.description);  // "Freezing"
Best Practices: Use getters for computed properties and data transformation. Use setters for validation and side effects. Getters should be fast (cache if expensive). Avoid side effects in getters when possible. Combine with private fields for true encapsulation. Setters can coerce values or throw errors.

6. Class Expressions and Anonymous Classes

Class Expression Types

Type Syntax Name Use Case
Named Class Expression const C = class Name {} Name only inside class Recursion, debugging
Anonymous Class Expression const C = class {} No internal name Simple assignments
IIFE Class (class {})() Anonymous Immediate instantiation
Inline Class func(class {}) Anonymous Factory functions, callbacks

Class Expression vs Declaration

Feature Class Declaration Class Expression
Hoisting Not hoisted (TDZ) Not hoisted (like var/let/const)
Name Required Optional
Assignment Creates binding Can assign to variable
Inline Usage ❌ Cannot use inline ✓ Can use as expression
Conditional ❌ Cannot be conditional ✓ Can be conditional

Example: Class expressions

// Anonymous class expression
const Person1 = class {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

const p1 = new Person1('Alice');
console.log(p1.greet());  // "Hello, I'm Alice"
console.log(Person1.name);  // "" (anonymous)

// Named class expression
const Person2 = class PersonClass {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        // Name available inside class
        return `Hello from ${PersonClass.name}`;
    }
    
    clone() {
        // Can use internal name for recursion
        return new PersonClass(this.name);
    }
};

const p2 = new Person2('Bob');
console.log(p2.greet());  // "Hello from PersonClass"
console.log(Person2.name);  // "PersonClass"

// Internal name not accessible outside
// console.log(PersonClass);  // ReferenceError

// Reassignment
const OldPerson = Person2;
const Person3 = class extends OldPerson {
    greet() {
        return super.greet() + ' (extended)';
    }
};

const p3 = new Person3('Charlie');
console.log(p3.greet());  // "Hello from PersonClass (extended)"

Example: Conditional class creation

// Create different classes based on condition
function createUserClass(type) {
    if (type === 'admin') {
        return class AdminUser {
            constructor(name) {
                this.name = name;
                this.role = 'admin';
            }
            
            manage() {
                return `${this.name} is managing`;
            }
        };
    } else {
        return class RegularUser {
            constructor(name) {
                this.name = name;
                this.role = 'user';
            }
            
            view() {
                return `${this.name} is viewing`;
            }
        };
    }
}

const AdminClass = createUserClass('admin');
const UserClass = createUserClass('user');

const admin = new AdminClass('Alice');
const user = new UserClass('Bob');

console.log(admin.manage());  // "Alice is managing"
console.log(user.view());     // "Bob is viewing"

// Environment-specific classes
const Storage = typeof window !== 'undefined'
    ? class BrowserStorage {
        save(key, value) {
            localStorage.setItem(key, value);
        }
        load(key) {
            return localStorage.getItem(key);
        }
    }
    : class NodeStorage {
        #data = new Map();
        
        save(key, value) {
            this.#data.set(key, value);
        }
        load(key) {
            return this.#data.get(key);
        }
    };

const storage = new Storage();
storage.save('key', 'value');

Example: Inline and IIFE classes

// Inline class as argument
function createInstance(ClassDef, ...args) {
    return new ClassDef(...args);
}

const point = createInstance(class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    
    distance() {
        return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
}, 3, 4);

console.log(point.distance());  // 5

// IIFE class - immediate instantiation
const singleton = new class Singleton {
    constructor() {
        this.id = Math.random();
        this.createdAt = Date.now();
    }
    
    getId() {
        return this.id;
    }
}();

console.log(singleton.getId());  // Random number

// Array of different classes
const shapes = [
    new (class Circle {
        constructor(r) { this.radius = r; }
        area() { return Math.PI * this.radius ** 2; }
    })(5),
    
    new (class Rectangle {
        constructor(w, h) { this.width = w; this.height = h; }
        area() { return this.width * this.height; }
    })(4, 6),
    
    new (class Triangle {
        constructor(b, h) { this.base = b; this.height = h; }
        area() { return 0.5 * this.base * this.height; }
    })(3, 4)
];

shapes.forEach(shape => console.log(shape.area()));
// 78.54, 24, 6

// Factory with inline class
function createPlugin(name, handler) {
    return new (class Plugin {
        constructor() {
            this.name = name;
            this.handler = handler;
        }
        
        execute(...args) {
            return this.handler(...args);
        }
    })();
}

const logger = createPlugin('logger', (msg) => console.log(`[LOG] ${msg}`));
logger.execute('Hello');  // "[LOG] Hello"

Example: Mixin pattern with class expressions

// Class expression for mixins
const Flyable = Base => class extends Base {
    fly() {
        return `${this.name} is flying`;
    }
    
    land() {
        return `${this.name} has landed`;
    }
};

const Swimmable = Base => class extends Base {
    swim() {
        return `${this.name} is swimming`;
    }
};

const Runnable = Base => class extends Base {
    run() {
        return `${this.name} is running`;
    }
};

// Base class
class Animal {
    constructor(name) {
        this.name = name;
    }
}

// Compose abilities
class Duck extends Swimmable(Flyable(Animal)) {
    quack() {
        return `${this.name} says quack`;
    }
}

class Fish extends Swimmable(Animal) {
    // Fish can only swim
}

class Bird extends Flyable(Runnable(Animal)) {
    // Bird can fly and run
}

const duck = new Duck('Donald');
console.log(duck.fly());   // "Donald is flying"
console.log(duck.swim());  // "Donald is swimming"
console.log(duck.quack()); // "Donald says quack"

const fish = new Fish('Nemo');
console.log(fish.swim());  // "Nemo is swimming"
// fish.fly();  // TypeError

const bird = new Bird('Tweety');
console.log(bird.fly());   // "Tweety is flying"
console.log(bird.run());   // "Tweety is running"
Use Cases: Class expressions enable conditional class creation, factory patterns, mixins, and inline class definitions. Named expressions provide internal name for recursion and debugging. Anonymous expressions are simpler for one-time use. IIFE classes for immediate instantiation.

7. Mixin Patterns and Class Composition

Mixin Approaches

Approach Method Pros Cons
Functional Mixin Function that modifies prototype Simple, flexible Pollutes prototype
Subclass Factory Function returning class extending Base Proper inheritance, composable More complex
Object.assign Copy methods to prototype Very simple Shallow, no super
Symbol-based Use symbols to avoid collisions No name collisions Less discoverable

Composition Patterns

Pattern Description Use When
Multiple Inheritance Combine multiple behaviors Need features from multiple sources
Trait-based Small reusable units of behavior Fine-grained feature composition
Delegation Forward calls to composed objects Prefer composition over inheritance
Strategy Swap implementations at runtime Need dynamic behavior changes

Example: Subclass factory mixins

// Mixin functions returning class
const TimestampMixin = Base => class extends Base {
    constructor(...args) {
        super(...args);
        this.createdAt = Date.now();
        this.updatedAt = Date.now();
    }
    
    touch() {
        this.updatedAt = Date.now();
    }
    
    getAge() {
        return Date.now() - this.createdAt;
    }
};

const ValidationMixin = Base => class extends Base {
    validate() {
        // Validate implementation
        return this.isValid();
    }
    
    isValid() {
        // Override in subclass
        return true;
    }
};

const SerializableMixin = Base => class extends Base {
    toJSON() {
        // Get all enumerable properties
        return Object.keys(this).reduce((obj, key) => {
            obj[key] = this[key];
            return obj;
        }, {});
    }
    
    static fromJSON(json) {
        const data = typeof json === 'string' ? JSON.parse(json) : json;
        return new this(data);
    }
};

// Base class
class Entity {
    constructor(id, name) {
        this.id = id;
        this.name = name;
    }
}

// Compose mixins
class User extends SerializableMixin(ValidationMixin(TimestampMixin(Entity))) {
    constructor(id, name, email) {
        super(id, name);
        this.email = email;
    }
    
    // Override validation
    isValid() {
        return this.email && this.email.includes('@');
    }
}

const user = new User(1, 'Alice', 'alice@example.com');
console.log(user.validate());  // true
console.log(user.getAge());    // Time since creation

user.touch();
console.log(user.updatedAt > user.createdAt);  // true

const json = user.toJSON();
console.log(json);  // {id: 1, name: "Alice", email: "alice@example.com", ...}

const restored = User.fromJSON(json);
console.log(restored.name);  // "Alice"

Example: Object.assign mixin pattern

// Simple mixin objects
const EventEmitterMixin = {
    on(event, handler) {
        this._handlers = this._handlers || {};
        this._handlers[event] = this._handlers[event] || [];
        this._handlers[event].push(handler);
        return this;
    },
    
    off(event, handler) {
        if (!this._handlers || !this._handlers[event]) return;
        
        if (handler) {
            const index = this._handlers[event].indexOf(handler);
            if (index > -1) {
                this._handlers[event].splice(index, 1);
            }
        } else {
            delete this._handlers[event];
        }
        return this;
    },
    
    emit(event, ...args) {
        if (!this._handlers || !this._handlers[event]) return;
        
        this._handlers[event].forEach(handler => {
            handler.apply(this, args);
        });
        return this;
    }
};

const LoggableMixin = {
    log(message) {
        console.log(`[${this.constructor.name}] ${message}`);
    },
    
    error(message) {
        console.error(`[${this.constructor.name}] ERROR: ${message}`);
    }
};

// Apply mixins to class
class Component {
    constructor(name) {
        this.name = name;
    }
}

// Mix in methods
Object.assign(Component.prototype, EventEmitterMixin, LoggableMixin);

const comp = new Component('MyComponent');

// Use mixed-in methods
comp.log('Component created');  // "[Component] Component created"

comp.on('update', (data) => {
    console.log('Updated:', data);
});

comp.emit('update', {value: 42});  // "Updated: {value: 42}"

// Helper function for multiple mixins
function applyMixins(targetClass, ...mixins) {
    mixins.forEach(mixin => {
        Object.assign(targetClass.prototype, mixin);
    });
}

class App {
    constructor(name) {
        this.name = name;
    }
}

applyMixins(App, EventEmitterMixin, LoggableMixin);

const app = new App('MyApp');
app.log('App started');
app.on('error', (err) => app.error(err.message));

Example: Functional composition pattern

// Functional mixin - modifies instance
function withId(obj) {
    obj.id = Math.random().toString(36).substr(2, 9);
    obj.getId = function() { return this.id; };
    return obj;
}

function withTimestamp(obj) {
    obj.timestamp = Date.now();
    obj.getTimestamp = function() { return this.timestamp; };
    return obj;
}

function withVersion(obj) {
    obj.version = '1.0.0';
    obj.getVersion = function() { return this.version; };
    return obj;
}

// Composition function
function compose(...funcs) {
    return (obj) => funcs.reduce((acc, func) => func(acc), obj);
}

const enhance = compose(withId, withTimestamp, withVersion);

// Use with any object
const data = {name: 'Test'};
const enhanced = enhance(data);

console.log(enhanced.getId());        // Random ID
console.log(enhanced.getTimestamp()); // Timestamp
console.log(enhanced.getVersion());   // "1.0.0"

// Pipeline pattern
const pipeline = (...fns) => x => fns.reduce((v, f) => f(v), x);

const createUser = pipeline(
    (name) => ({name}),
    withId,
    withTimestamp,
    (obj) => {
        obj.greet = function() { return `Hello, ${this.name}`; };
        return obj;
    }
);

const user2 = createUser('Bob');
console.log(user2.greet());  // "Hello, Bob"
console.log(user2.id);       // Random ID

Example: Advanced mixin composition

// Mixin with dependencies
const RequiresName = Base => class extends Base {
    constructor(...args) {
        super(...args);
        if (!this.name) {
            throw new Error('Name is required');
        }
    }
};

const Describable = Base => class extends Base {
    describe() {
        return `${this.constructor.name}: ${this.name}`;
    }
};

const Comparable = Base => class extends Base {
    compareTo(other) {
        if (this.name < other.name) return -1;
        if (this.name > other.name) return 1;
        return 0;
    }
    
    equals(other) {
        return this.compareTo(other) === 0;
    }
};

// Composable mixin builder
function mix(baseClass) {
    return {
        with(...mixins) {
            return mixins.reduce((c, mixin) => mixin(c), baseClass);
        }
    };
}

// Build class with mixins
class Item {
    constructor(name, value) {
        this.name = name;
        this.value = value;
    }
}

const EnhancedItem = mix(Item)
    .with(RequiresName, Describable, Comparable);

const item1 = new EnhancedItem('Apple', 10);
const item2 = new EnhancedItem('Banana', 5);

console.log(item1.describe());      // "EnhancedItem: Apple"
console.log(item1.compareTo(item2)); // -1 (Apple < Banana)

// Conditional mixins
function withFeature(feature, Mixin) {
    return Base => {
        if (feature.enabled) {
            return Mixin(Base);
        }
        return Base;
    };
}

const DebugMixin = Base => class extends Base {
    debug(msg) {
        console.log(`[DEBUG] ${msg}`);
    }
};

const config = {debug: {enabled: true}};

const DebugItem = mix(Item)
    .with(withFeature(config.debug, DebugMixin));

const debugItem = new DebugItem('Test', 1);
if (debugItem.debug) {
    debugItem.debug('Item created');  // "[DEBUG] Item created"
}

Example: Trait-based composition

// Trait objects (small, focused behaviors)
const Clickable = {
    click() {
        this.emit('click', this);
    },
    
    onClick(handler) {
        this.on('click', handler);
        return this;
    }
};

const Hoverable = {
    hover() {
        this.emit('hover', this);
    },
    
    onHover(handler) {
        this.on('hover', handler);
        return this;
    }
};

const Draggable = {
    startDrag(x, y) {
        this.dragStart = {x, y};
        this.emit('dragstart', this);
    },
    
    drag(x, y) {
        if (!this.dragStart) return;
        this.emit('drag', {x, y, start: this.dragStart});
    },
    
    endDrag() {
        this.dragStart = null;
        this.emit('dragend', this);
    }
};

// Trait composer
function withTraits(target, ...traits) {
    traits.forEach(trait => {
        Object.keys(trait).forEach(key => {
            if (key in target.prototype) {
                console.warn(`Overriding ${key} in ${target.name}`);
            }
            target.prototype[key] = trait[key];
        });
    });
    return target;
}

// Base with event system
class UIElement {
    constructor(type) {
        this.type = type;
        this._handlers = {};
    }
    
    on(event, handler) {
        this._handlers[event] = this._handlers[event] || [];
        this._handlers[event].push(handler);
        return this;
    }
    
    emit(event, data) {
        if (this._handlers[event]) {
            this._handlers[event].forEach(h => h(data));
        }
        return this;
    }
}

// Compose specific elements
const Button = withTraits(
    class Button extends UIElement {
        constructor(label) {
            super('button');
            this.label = label;
        }
    },
    Clickable,
    Hoverable
);

const Panel = withTraits(
    class Panel extends UIElement {
        constructor(title) {
            super('panel');
            this.title = title;
        }
    },
    Clickable,
    Hoverable,
    Draggable
);

const btn = new Button('Click Me');
btn.onClick(() => console.log('Button clicked'));
btn.click();  // "Button clicked"

const panel = new Panel('My Panel');
panel.onClick(() => console.log('Panel clicked'));
panel.onHover(() => console.log('Panel hovered'));

panel.startDrag(0, 0);
panel.drag(10, 10);
panel.endDrag();
Best Practices: Prefer composition over inheritance for flexibility. Use subclass factory mixins for proper inheritance chain. Keep mixins small and focused (single responsibility). Document mixin dependencies. Avoid naming conflicts (use symbols if needed). Consider trait-based composition for fine-grained features. Use composition for "has-a" relationships, inheritance for "is-a".

Section 17 Summary

  • Classes: Syntactic sugar over prototypes; not hoisted; always strict mode; constructor initializes; methods on prototype; fields on instance
  • Methods: Instance methods (prototype, access this), static methods (class itself, utilities/factories), inherited by subclasses
  • Inheritance: extends creates subclass; super() calls parent constructor (required before this); super.method() calls parent methods
  • Private Members: #field and #method truly private; not inherited; separate namespace per class; use for encapsulation
  • Getters/Setters: get/set for computed properties, validation, side effects; access like properties; can be private/static
  • Class Expressions: Named or anonymous; enable conditional creation, factories, inline usage; IIFE classes for immediate instantiation
  • Mixins: Subclass factory pattern for composition; Object.assign for simple cases; functional composition; trait-based for fine-grained features
  • Composition: Prefer composition over inheritance for flexibility; combine multiple behaviors; avoid deep inheritance hierarchies; mix(...).with(mixins) pattern