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 classclass 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 instancesconst alice = new Person('Alice', 30);const bob = new Person('Bob', 25);console.log(alice.greet()); // "Hello, I'm Alice"console.log(alice.getAge()); // 30console.log(bob.greet()); // "Hello, I'm Bob"// Methods are on prototypeconsole.log(alice.greet === bob.greet); // true (shared method)// instanceof checkconsole.log(alice instanceof Person); // true// Class is not hoisted// const p = new MyClass(); // ReferenceError// class MyClass {}// Classes are strict mode by defaultclass 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); // timestampconst user2 = new User(2, 'Bob');console.log(user2.role); // "user" (default)// Fields are instance properties (not on prototype)console.log(user1.hasOwnProperty('name')); // trueconsole.log('name' in User.prototype); // false// Methods are on prototypeconsole.log(user1.hasOwnProperty('getInfo')); // falseconsole.log('getInfo' in User.prototype); // true
Example: Constructor patterns and validation
// Constructor with validationclass 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 parametersclass 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 destructuringclass 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 parametersclass 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 instanceclass Normal { constructor(value) { this.value = value; }}const n = new Normal(5);console.log(n instanceof Normal); // trueconsole.log(n.value); // 5// Constructor returning object - overrides instanceclass 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); // 10console.log(c.custom); // true// Returning primitive - ignoredclass Primitive { constructor(value) { this.value = value; return 42; // Ignored }}const p = new Primitive(5);console.log(p instanceof Primitive); // trueconsole.log(p.value); // 5// Singleton pattern with constructorclass 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 prototypeconsole.log(calc.hasOwnProperty('add')); // falseconsole.log('add' in Calculator.prototype); // true// Shared across instancesconst 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)); // 8console.log(MathUtils.max(1, 5, 3, 9, 2)); // 9console.log(MathUtils.clamp(15, 0, 10)); // 10console.log(MathUtils.average(1, 2, 3, 4)); // 2.5// Static methods are on class, not prototypeconsole.log('add' in MathUtils); // trueconsole.log('add' in MathUtils.prototype); // false// Can't call on instancesconst util = new MathUtils();// util.add(1, 2); // TypeError: util.add is not a function// Factory pattern with static methodsclass 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"
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 inheritedconsole.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 instancesconsole.log(Dog.compare(dog1, dog2)); // Compares names// Instance methodsconsole.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 classclass Animal { constructor(name, age) { this.name = name; this.age = age; } speak() { return `${this.name} makes a sound`; } getAge() { return this.age; }}// Child classclass 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 chainconsole.log(dog instanceof Dog); // trueconsole.log(dog instanceof Animal); // trueconsole.log(dog instanceof Object); // true// Inheritance chainconsole.log(Object.getPrototypeOf(dog) === Dog.prototype); // trueconsole.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 exampleclass 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 inheritanceclass 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 chainconsole.log(dog2 instanceof Dog2); // trueconsole.log(dog2 instanceof Animal2); // trueconsole.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 inheritedUser2.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 correctlyconst 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)
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); // 1console.log(obj2.id); // 2console.log(obj3.id); // 3console.log(IDGenerator.getTotalGenerated()); // 3console.log(IDGenerator.getActiveCount()); // 3IDGenerator.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 namespaceconsole.log(derived.getPrivateBase()); // "base private"console.log(derived.getDerivedPrivateBase()); // "derived has its own #privateBase"
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 setterperson.fullName = 'Jane Smith';console.log(person.firstName); // "Jane"console.log(person.lastName); // "Smith"console.log(person.fullName); // "Jane Smith"// Read-only propertyperson.initials = 'X.Y.'; // Ignored (non-strict) or error (strict)console.log(person.initials); // Still "J.S."// Age is computed each timeconsole.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.99console.log(product.quantity); // 5console.log(product.total); // 99.95console.log(product.formattedTotal); // "$99.95"// Validation in actiontry { 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);}
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 classconsole.log(Configuration.apiUrl); // "https://api.example.com"console.log(Configuration.timeout); // 5000Configuration.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); // 25console.log(temp.fahrenheit); // 77console.log(temp.kelvin); // 298.15console.log(temp.description); // "Warm"console.log(temp.toString()); // "25.0°C (77.0°F)"temp.fahrenheit = 32; // Set via Fahrenheitconsole.log(temp.celsius); // 0console.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 expressionconst 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 expressionconst 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// Reassignmentconst 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 conditionfunction 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 classesconst 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');
// Class expression for mixinsconst 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 classclass Animal { constructor(name) { this.name = name; }}// Compose abilitiesclass 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(); // TypeErrorconst 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 classconst 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 classclass Entity { constructor(id, name) { this.id = id; this.name = name; }}// Compose mixinsclass 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()); // trueconsole.log(user.getAge()); // Time since creationuser.touch();console.log(user.updatedAt > user.createdAt); // trueconst json = user.toJSON();console.log(json); // {id: 1, name: "Alice", email: "alice@example.com", ...}const restored = User.fromJSON(json);console.log(restored.name); // "Alice"
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