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:
extendscreates subclass;super()calls parent constructor (required before this);super.method()calls parent methods - Private Members:
#fieldand#methodtruly private; not inherited; separate namespace per class; use for encapsulation - Getters/Setters:
get/setfor 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.assignfor 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