JS Classes & Object-Oriented Programming
Master modern ECMAScript Class architecture: constructors, instance methods, static members, inheritance with extends and super(), true private fields and methods (#private), getters/setters, static initialization blocks, and polymorphism.
Class Hierarchy & Memory Structure
Under the hood, ES6 class syntax is elegant syntactic sugar over prototypes. Subclasses establish dual prototype links: one between instance prototypes for shared methods, and another between constructor functions for static inheritance.
1. Anatomy of Modern JavaScript Classes
The class keyword introduces a clean, unified syntax for defining object constructors, instance fields, methods, and getters/setters:
Complete Modern Class Declaration
class BankAccount {
// 1. Public Class Field (ES2022)
accountType = 'SAVINGS';
// 2. Private Field (Cannot be accessed outside this class!)
#balance = 0;
#accountNumber;
// 3. Static Property (Belongs to class, not instances)
static minimumDeposit = 50;
// 4. Constructor (Instantiates new objects)
constructor(accountNumber, initialDeposit = 0) {
if (initialDeposit < BankAccount.minimumDeposit) {
throw new Error(`Minimum deposit is $${BankAccount.minimumDeposit}`);
}
this.#accountNumber = accountNumber;
this.#balance = initialDeposit;
}
// 5. Getter & Setter (Virtual properties)
get balance() {
return this.#balance;
}
// 6. Instance Method
deposit(amount) {
if (amount <= 0) throw new Error('Invalid deposit');
this.#balance += amount;
return this.#balance;
}
}
const myAcct = new BankAccount('ACC-101', 200);
myAcct.deposit(100);
console.log('Balance:', myAcct.balance); // 300
// myAcct.#balance -> SyntaxError: Private field '#balance' must be declared in an enclosing class
Try it in Playground
2. Inheritance with extends & super()
JavaScript classes support single inheritance using extends. In a derived subclass, the super() constructor call is mandatory before accessing this:
Subclassing & Polymorphism
class Employee {
constructor(name, baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
calculatePay() {
return this.baseSalary;
}
}
class Manager extends Employee {
#bonus = 0;
constructor(name, baseSalary, bonus) {
// CRITICAL: Must invoke super() before touching 'this'!
super(name, baseSalary);
this.#bonus = bonus;
}
// Polymorphic method override:
calculatePay() {
// Calling super.method() invokes parent implementation:
return super.calculatePay() + this.#bonus;
}
}
const emp = new Employee('Bob', 5000);
const mgr = new Manager('Alice', 8000, 3000);
console.log(emp.calculatePay()); // 5000
console.log(mgr.calculatePay()); // 11000 (8000 + 3000)
console.log(mgr instanceof Employee); // true (Inherits prototype!)
Try it in Playground
super() Order Rulethis.property before calling super() throws a ReferenceError: Must call super constructor in derived class before accessing 'this'.
3. Static Members & Static Initialization Blocks (ES2022)
Static methods and fields are attached directly to the constructor function rather than to instances. ES2022 added Static Initialization Blocks to permit complex static bootstrapping with private access:
Static Factory & Static Blocks
class DatabaseConnection {
static #pool = [];
static maxConnections;
// ES2022 Static Initialization Block:
static {
// Runs once when class is evaluated:
try {
this.maxConnections = 10;
console.log('Database pool initialized with limit:', this.maxConnections);
} catch {
this.maxConnections = 1;
}
}
// Static Factory Method:
static create(connectionString) {
if (this.#pool.length >= this.maxConnections) {
throw new Error('Connection pool exhausted!');
}
const conn = new DatabaseConnection(connectionString);
this.#pool.push(conn);
return conn;
}
constructor(connString) {
this.connString = connString;
}
}
const db = DatabaseConnection.create('mongodb://localhost:27017');
Try it in Playground
Interactive Mini-Lab: OOP Hierarchy Simulator
Instantiate classes dynamically, test polymorphic method dispatch, and verify true private field enforcement:
Hands-on Challenge: Production EventEmitter Class
Coding Challenge
Build a production-grade EventEmitter class using modern ES6 syntax and true private state. The class must manage an internal subscriber dictionary without exposing internal arrays to outside manipulation:
- Store events in a private
#events = new Map();field. on(eventName, listener): registers an event listener callback.emit(eventName, ...args): invokes all registered listeners synchronously with passed arguments.off(eventName, listener): removes a specific registered listener.once(eventName, listener): registers a listener that automatically removes itself after its first execution!
/**
* Professional EventEmitter class utilizing true private fields (#).
*/
class EventEmitter {
// Private listener dictionary:
#events = new Map();
on(event, listener) {
if (typeof listener !== 'function') throw new TypeError('Listener must be a function');
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(listener);
return this; // Enable method chaining
}
emit(event, ...args) {
const listeners = this.#events.get(event);
if (!listeners || listeners.length === 0) return false;
// Iterate over a copy so listeners modifying the array during emit don't cause skip bugs:
[...listeners].forEach(listener => listener(...args));
return true;
}
off(event, listener) {
const listeners = this.#events.get(event);
if (!listeners) return this;
const filtered = listeners.filter(fn => fn !== listener);
if (filtered.length === 0) {
this.#events.delete(event);
} else {
this.#events.set(event, filtered);
}
return this;
}
once(event, listener) {
const wrapper = (...args) => {
this.off(event, wrapper);
listener(...args);
};
return this.on(event, wrapper);
}
}
// Verification:
const emitter = new EventEmitter();
const onUserLogin = user => console.log('Welcome:', user.name);
emitter.on('login', onUserLogin);
emitter.emit('login', { name: 'Sarah' }); // Welcome: Sarah
emitter.off('login', onUserLogin);
emitter.emit('login', { name: 'Sarah' }); // Silent (unsubscribed)
Chapter 23 Knowledge Check
Validate your mastery of ES6 classes, inheritance, and object-oriented principles.
this.name = 'Bob' BEFORE calling super()?name.
ReferenceError is thrown immediately.
_password)
#password)
private password
$password)
const u = new User(); u.staticMethod()
User.staticMethod()
super() call in subclasses.
call() or apply() methods exclusively.
class?static { ... }) execute?new.