javascript
examples
examples.js⚡javascript
/**
* ========================================
* 8.4 PRIVATE FIELDS & METHODS - EXAMPLES
* ========================================
*/
/**
* EXAMPLE 1: Basic Private Fields
* Using # prefix for private instance fields
*/
console.log('--- Example 1: Basic Private Fields ---');
class Person {
#ssn; // Private field - must be declared
name; // Public field
constructor(name, ssn) {
this.name = name;
this.#ssn = ssn;
}
// Public method to access private data
getSSNLast4() {
return '***-**-' + this.#ssn.slice(-4);
}
// Private data can only be accessed inside class
verifySSN(ssn) {
return this.#ssn === ssn;
}
}
const person = new Person('Alice', '123-45-6789');
console.log(person.name); // "Alice"
console.log(person.getSSNLast4()); // "***-**-6789"
console.log(person.verifySSN('123-45-6789')); // true
// Cannot access private field externally
// console.log(person.#ssn); // SyntaxError!
console.log(person.ssn); // undefined (not the private field)
/**
* EXAMPLE 2: Multiple Private Fields
* Encapsulating related data
*/
console.log('\n--- Example 2: Multiple Private Fields ---');
class BankAccount {
#accountNumber;
#balance;
#pin;
#transactionHistory = [];
constructor(accountNumber, initialBalance, pin) {
this.#accountNumber = accountNumber;
this.#balance = initialBalance;
this.#pin = pin;
}
// Getters for controlled access
get accountNumber() {
const masked = this.#accountNumber.slice(-4);
return `****${masked}`;
}
get balance() {
return this.#balance;
}
// PIN verification
#verifyPin(pin) {
return this.#pin === pin;
}
// Transaction logging
#logTransaction(type, amount) {
this.#transactionHistory.push({
type,
amount,
balance: this.#balance,
date: new Date(),
});
}
deposit(amount) {
if (amount <= 0) throw new Error('Invalid amount');
this.#balance += amount;
this.#logTransaction('deposit', amount);
return this.#balance;
}
withdraw(amount, pin) {
if (!this.#verifyPin(pin)) {
throw new Error('Invalid PIN');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
this.#logTransaction('withdrawal', amount);
return amount;
}
getStatement() {
return this.#transactionHistory.map((t) => ({
...t,
date: t.date.toISOString(),
}));
}
}
const account = new BankAccount('1234567890', 1000, '1234');
console.log('Account:', account.accountNumber); // "****7890"
console.log('Balance:', account.balance); // 1000
account.deposit(500);
account.withdraw(200, '1234');
console.log('After transactions:', account.balance); // 1300
console.log('Statement:', account.getStatement());
/**
* EXAMPLE 3: Private Methods
* Internal implementation details hidden
*/
console.log('\n--- Example 3: Private Methods ---');
class PasswordManager {
#passwords = new Map();
#masterKey;
constructor(masterKey) {
this.#masterKey = masterKey;
}
// Private encryption/decryption methods
#encrypt(text) {
// Simplified encryption (use proper crypto in production)
return Buffer.from(text).toString('base64');
}
#decrypt(encrypted) {
return Buffer.from(encrypted, 'base64').toString();
}
// Private validation
#validateMasterKey(key) {
return key === this.#masterKey;
}
// Public interface
store(service, password, masterKey) {
if (!this.#validateMasterKey(masterKey)) {
throw new Error('Invalid master key');
}
this.#passwords.set(service, this.#encrypt(password));
}
retrieve(service, masterKey) {
if (!this.#validateMasterKey(masterKey)) {
throw new Error('Invalid master key');
}
const encrypted = this.#passwords.get(service);
if (!encrypted) return null;
return this.#decrypt(encrypted);
}
listServices() {
return [...this.#passwords.keys()];
}
}
const pm = new PasswordManager('secret123');
pm.store('email', 'myEmailPass', 'secret123');
pm.store('github', 'myGithubPass', 'secret123');
console.log('Services:', pm.listServices());
console.log('Email password:', pm.retrieve('email', 'secret123'));
/**
* EXAMPLE 4: Private Static Members
* Class-level private data
*/
console.log('\n--- Example 4: Private Static Members ---');
class IDGenerator {
static #counter = 0;
static #prefix = 'ID';
static #separator = '-';
// Private static method
static #format(num) {
return num.toString().padStart(6, '0');
}
// Public interface
static next() {
return `${this.#prefix}${this.#separator}${this.#format(++this.#counter)}`;
}
static setPrefix(prefix) {
this.#prefix = prefix;
}
static get count() {
return this.#counter;
}
}
console.log(IDGenerator.next()); // "ID-000001"
console.log(IDGenerator.next()); // "ID-000002"
IDGenerator.setPrefix('USER');
console.log(IDGenerator.next()); // "USER-000003"
console.log('Total generated:', IDGenerator.count);
// Cannot access private static
// IDGenerator.#counter; // SyntaxError
/**
* EXAMPLE 5: Private Getters and Setters
* Internal computed properties
*/
console.log('\n--- Example 5: Private Getters/Setters ---');
class Circle {
#radius;
constructor(radius) {
this.#radius = radius;
}
// Private getter
get #diameter() {
return this.#radius * 2;
}
// Private setter
set #diameter(value) {
this.#radius = value / 2;
}
// Public interface using private accessors
get radius() {
return this.#radius;
}
set radius(value) {
if (value <= 0) throw new Error('Radius must be positive');
this.#radius = value;
}
get area() {
return Math.PI * this.#radius ** 2;
}
get circumference() {
return Math.PI * this.#diameter;
}
scale(factor) {
this.#diameter = this.#diameter * factor;
return this;
}
}
const circle = new Circle(5);
console.log('Radius:', circle.radius);
console.log('Area:', circle.area.toFixed(2));
console.log('Circumference:', circle.circumference.toFixed(2));
circle.scale(2);
console.log('After scaling - Radius:', circle.radius); // 10
/**
* EXAMPLE 6: Checking for Private Fields
* Using `in` operator with private fields
*/
console.log('\n--- Example 6: Checking Private Fields ---');
class User {
#id;
#isAdmin;
constructor(id, isAdmin = false) {
this.#id = id;
this.#isAdmin = isAdmin;
}
get id() {
return this.#id;
}
// Check if object has #id field
static isUser(obj) {
return #id in obj;
}
// Check if object has #isAdmin field
static hasAdminField(obj) {
return #isAdmin in obj;
}
}
const user = new User(1);
const fakeUser = { id: 1 };
console.log('user is User:', User.isUser(user)); // true
console.log('fakeUser is User:', User.isUser(fakeUser)); // false
/**
* EXAMPLE 7: Private Fields in Inheritance
* Subclass cannot access parent's private fields
*/
console.log('\n--- Example 7: Private Fields in Inheritance ---');
class Parent {
#privateField = 'parent private';
_protectedField = 'parent protected'; // Convention
publicField = 'parent public';
getPrivate() {
return this.#privateField;
}
#privateMethod() {
return 'Called parent private method';
}
callPrivateMethod() {
return this.#privateMethod();
}
}
class Child extends Parent {
#childPrivate = 'child private';
showFields() {
console.log('Public:', this.publicField); // Works
console.log('Protected (convention):', this._protectedField); // Works
// console.log("Private:", this.#privateField); // SyntaxError!
// Access via inherited method
console.log('Parent private via method:', this.getPrivate());
console.log('Parent private method:', this.callPrivateMethod());
}
}
const child = new Child();
child.showFields();
/**
* EXAMPLE 8: Immutable-like Objects with Private Fields
* Preventing external mutation
*/
console.log('\n--- Example 8: Immutable-like Objects ---');
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
Object.freeze(this); // Prevent adding new properties
}
get x() {
return this.#x;
}
get y() {
return this.#y;
}
// Returns new instances instead of mutating
add(point) {
return new Point(this.#x + point.x, this.#y + point.y);
}
subtract(point) {
return new Point(this.#x - point.x, this.#y - point.y);
}
scale(factor) {
return new Point(this.#x * factor, this.#y * factor);
}
distance(point) {
const dx = this.#x - point.x;
const dy = this.#y - point.y;
return Math.sqrt(dx * dx + dy * dy);
}
toString() {
return `Point(${this.#x}, ${this.#y})`;
}
}
const p1 = new Point(3, 4);
const p2 = new Point(6, 8);
const p3 = p1.add(p2);
console.log(p1.toString()); // Point(3, 4) - unchanged
console.log(p3.toString()); // Point(9, 12) - new point
console.log('Distance:', p1.distance(p2));
// Cannot modify
// p1.x = 10; // Fails silently (frozen) or throws in strict mode
/**
* EXAMPLE 9: WeakMap Alternative Pattern
* Pre-ES2022 way to achieve privacy
*/
console.log('\n--- Example 9: WeakMap Alternative ---');
// Old pattern using WeakMap
const _data = new WeakMap();
class PersonOld {
constructor(name, secret) {
_data.set(this, { name, secret });
}
get name() {
return _data.get(this).name;
}
getSecret(password) {
if (password !== 'opensesame') {
throw new Error('Wrong password');
}
return _data.get(this).secret;
}
}
// Modern pattern using #
class PersonNew {
#name;
#secret;
constructor(name, secret) {
this.#name = name;
this.#secret = secret;
}
get name() {
return this.#name;
}
getSecret(password) {
if (password !== 'opensesame') {
throw new Error('Wrong password');
}
return this.#secret;
}
}
const oldPerson = new PersonOld('Alice', 'I love cats');
const newPerson = new PersonNew('Bob', 'I love dogs');
console.log('Old pattern:', oldPerson.name);
console.log('New pattern:', newPerson.name);
console.log('Old secret:', oldPerson.getSecret('opensesame'));
console.log('New secret:', newPerson.getSecret('opensesame'));
/**
* EXAMPLE 10: State Machine with Private State
* Encapsulating complex state transitions
*/
console.log('\n--- Example 10: State Machine ---');
class TrafficLight {
static #STATES = ['green', 'yellow', 'red'];
static #DURATIONS = {
green: 3000,
yellow: 1000,
red: 2000,
};
#currentIndex = 0;
#timerId = null;
#isRunning = false;
constructor() {
// Initial state
}
get state() {
return TrafficLight.#STATES[this.#currentIndex];
}
#transition() {
this.#currentIndex = (this.#currentIndex + 1) % TrafficLight.#STATES.length;
console.log(`Light changed to: ${this.state}`);
return this.state;
}
#scheduleNext() {
if (!this.#isRunning) return;
const duration = TrafficLight.#DURATIONS[this.state];
this.#timerId = setTimeout(() => {
this.#transition();
this.#scheduleNext();
}, duration);
}
start() {
if (this.#isRunning) return;
this.#isRunning = true;
console.log(`Starting at: ${this.state}`);
this.#scheduleNext();
}
stop() {
this.#isRunning = false;
if (this.#timerId) {
clearTimeout(this.#timerId);
this.#timerId = null;
}
console.log('Traffic light stopped');
}
}
const light = new TrafficLight();
console.log('Current state:', light.state);
// Uncomment to run the traffic light
// light.start();
// setTimeout(() => light.stop(), 10000);
/**
* EXAMPLE 11: Validation with Private Methods
* Internal validation logic
*/
console.log('\n--- Example 11: Validation ---');
class UserForm {
#email;
#password;
#errors = [];
constructor(email, password) {
this.#errors = [];
if (this.#validateEmail(email)) {
this.#email = email;
}
if (this.#validatePassword(password)) {
this.#password = password;
}
}
// Private validation methods
#validateEmail(email) {
if (!email || !email.includes('@')) {
this.#errors.push('Invalid email format');
return false;
}
return true;
}
#validatePassword(password) {
const errors = [];
if (!password || password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain a number');
}
if (errors.length > 0) {
this.#errors.push(...errors);
return false;
}
return true;
}
get isValid() {
return this.#errors.length === 0;
}
get errors() {
return [...this.#errors];
}
getData() {
if (!this.isValid) {
throw new Error('Form is invalid');
}
return { email: this.#email, password: '****' };
}
}
const validForm = new UserForm('test@example.com', 'Password123');
console.log('Valid form:', validForm.isValid); // true
console.log('Data:', validForm.getData());
const invalidForm = new UserForm('invalid', 'weak');
console.log('Invalid form:', invalidForm.isValid); // false
console.log('Errors:', invalidForm.errors);
/**
* EXAMPLE 12: Event Emitter with Private Storage
* Encapsulated event handling
*/
console.log('\n--- Example 12: Event Emitter ---');
class EventEmitter {
#events = new Map();
#maxListeners = 10;
// Private helper methods
#getListeners(event) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
return this.#events.get(event);
}
#checkMaxListeners(event) {
const listeners = this.#getListeners(event);
if (listeners.length >= this.#maxListeners) {
console.warn(
`Warning: Event '${event}' has ${listeners.length} listeners. ` +
`Consider increasing maxListeners.`
);
}
}
// Public interface
on(event, callback) {
this.#checkMaxListeners(event);
this.#getListeners(event).push(callback);
return this;
}
off(event, callback) {
const listeners = this.#getListeners(event);
const index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
return this;
}
emit(event, ...args) {
for (const callback of this.#getListeners(event)) {
callback(...args);
}
return this;
}
once(event, callback) {
const wrapper = (...args) => {
this.off(event, wrapper);
callback(...args);
};
return this.on(event, wrapper);
}
setMaxListeners(n) {
this.#maxListeners = n;
return this;
}
}
const emitter = new EventEmitter();
emitter.on('message', (msg) => console.log('Received:', msg));
emitter.once('connect', () => console.log('Connected!'));
emitter.emit('connect');
emitter.emit('connect'); // Won't fire again
emitter.emit('message', 'Hello');
emitter.emit('message', 'World');
/**
* EXAMPLE 13: Cache with Private TTL Logic
* Encapsulated caching behavior
*/
console.log('\n--- Example 13: Cache with TTL ---');
class Cache {
#store = new Map();
#defaultTTL;
constructor(defaultTTL = 60000) {
// 1 minute default
this.#defaultTTL = defaultTTL;
}
// Private expiry check
#isExpired(entry) {
if (!entry.expiry) return false;
return Date.now() > entry.expiry;
}
// Private cleanup
#cleanup() {
for (const [key, entry] of this.#store) {
if (this.#isExpired(entry)) {
this.#store.delete(key);
}
}
}
set(key, value, ttl = this.#defaultTTL) {
this.#store.set(key, {
value,
expiry: ttl ? Date.now() + ttl : null,
createdAt: Date.now(),
});
return this;
}
get(key) {
const entry = this.#store.get(key);
if (!entry) return undefined;
if (this.#isExpired(entry)) {
this.#store.delete(key);
return undefined;
}
return entry.value;
}
has(key) {
return this.get(key) !== undefined;
}
delete(key) {
return this.#store.delete(key);
}
clear() {
this.#store.clear();
}
// Periodic cleanup
startCleanup(interval = 60000) {
setInterval(() => this.#cleanup(), interval);
}
get size() {
this.#cleanup();
return this.#store.size;
}
}
const cache = new Cache(1000); // 1 second TTL
cache.set('temp', 'temporary value');
cache.set('permanent', 'permanent value', null); // No expiry
console.log('Temp:', cache.get('temp')); // "temporary value"
console.log('Size:', cache.size); // 2
// Wait for expiry
setTimeout(() => {
console.log('After 1.5s - Temp:', cache.get('temp')); // undefined
console.log('After 1.5s - Permanent:', cache.get('permanent')); // "permanent value"
}, 1500);
/**
* EXAMPLE 14: Observable with Private Observers
* Reactive pattern with encapsulation
*/
console.log('\n--- Example 14: Observable ---');
class Observable {
#value;
#observers = new Set();
constructor(initialValue) {
this.#value = initialValue;
}
get value() {
return this.#value;
}
set value(newValue) {
const oldValue = this.#value;
this.#value = newValue;
this.#notify(newValue, oldValue);
}
#notify(newValue, oldValue) {
for (const observer of this.#observers) {
observer(newValue, oldValue);
}
}
subscribe(observer) {
this.#observers.add(observer);
// Return unsubscribe function
return () => this.#observers.delete(observer);
}
// Computed observable
map(transform) {
const computed = new Observable(transform(this.#value));
this.subscribe((newValue) => {
computed.value = transform(newValue);
});
return computed;
}
}
const count = new Observable(0);
const unsubscribe = count.subscribe((newVal, oldVal) => {
console.log(`Count changed: ${oldVal} → ${newVal}`);
});
const doubled = count.map((x) => x * 2);
doubled.subscribe((val) => console.log(`Doubled: ${val}`));
count.value = 5;
count.value = 10;
unsubscribe();
count.value = 15; // No log from first observer
/**
* EXAMPLE 15: Complete Example - Secure Session Manager
* Comprehensive use of private fields and methods
*/
console.log('\n--- Example 15: Secure Session Manager ---');
class SessionManager {
static #instance = null;
static #SECRET_KEY = 'app-secret-key-123';
#sessions = new Map();
#tokenExpiry = 3600000; // 1 hour
constructor() {
if (SessionManager.#instance) {
return SessionManager.#instance;
}
SessionManager.#instance = this;
}
static getInstance() {
if (!SessionManager.#instance) {
new SessionManager();
}
return SessionManager.#instance;
}
// Private token generation
#generateToken() {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let token = '';
for (let i = 0; i < 32; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
}
// Private session validation
#isValidSession(session) {
if (!session) return false;
if (Date.now() > session.expiresAt) {
return false;
}
return true;
}
// Private session cleanup
#cleanupExpired() {
for (const [token, session] of this.#sessions) {
if (!this.#isValidSession(session)) {
this.#sessions.delete(token);
}
}
}
createSession(userId, data = {}) {
const token = this.#generateToken();
const session = {
userId,
data,
createdAt: Date.now(),
expiresAt: Date.now() + this.#tokenExpiry,
};
this.#sessions.set(token, session);
return token;
}
getSession(token) {
const session = this.#sessions.get(token);
if (!this.#isValidSession(session)) {
this.#sessions.delete(token);
return null;
}
return { ...session };
}
updateSession(token, data) {
const session = this.#sessions.get(token);
if (!this.#isValidSession(session)) {
throw new Error('Invalid or expired session');
}
session.data = { ...session.data, ...data };
return true;
}
destroySession(token) {
return this.#sessions.delete(token);
}
get activeSessions() {
this.#cleanupExpired();
return this.#sessions.size;
}
}
const sessionMgr = SessionManager.getInstance();
const token = sessionMgr.createSession('user123', { role: 'admin' });
console.log('Token:', token);
console.log('Session:', sessionMgr.getSession(token));
console.log('Active sessions:', sessionMgr.activeSessions);
sessionMgr.updateSession(token, { lastAction: 'login' });
console.log('Updated session:', sessionMgr.getSession(token));
/**
* EXAMPLE 16: Private Fields with Proxy
* Note: Proxies have limitations with private fields
*/
console.log('\n--- Example 16: Private Fields with Proxy ---');
class Counter {
#count = 0;
increment() {
this.#count++;
return this.#count;
}
get count() {
return this.#count;
}
}
const counter = new Counter();
// Creating a proxy for logging
const loggedCounter = new Proxy(counter, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function (...args) {
console.log(`Calling ${String(prop)}()`);
// Must call with original target for private fields
return value.apply(target, args);
};
}
return value;
},
});
console.log('Via proxy:', loggedCounter.increment()); // Works
console.log('Count via proxy:', loggedCounter.count); // Works
/**
* EXAMPLE 17: Symbol vs Private Fields Comparison
* When to use which approach
*/
console.log('\n--- Example 17: Symbol vs Private Fields ---');
// Symbol approach (discoverable via reflection)
const _secretSym = Symbol('secret');
class SymbolPrivate {
constructor(secret) {
this[_secretSym] = secret;
}
getSecret() {
return this[_secretSym];
}
}
// True private fields (not discoverable)
class TruePrivate {
#secret;
constructor(secret) {
this.#secret = secret;
}
getSecret() {
return this.#secret;
}
}
const symInstance = new SymbolPrivate('symbol secret');
const privateInstance = new TruePrivate('private secret');
// Symbol properties are discoverable
console.log('Symbol props:', Object.getOwnPropertySymbols(symInstance)); // [Symbol(secret)]
// Private fields are not
console.log('Own props:', Object.getOwnPropertyNames(privateInstance)); // []
console.log('Keys:', Object.keys(privateInstance)); // []
/**
* EXAMPLE 18: Private Methods for Strategy Pattern
* Encapsulating strategy implementations
*/
console.log('\n--- Example 18: Strategy with Private Methods ---');
class Sorter {
#strategy = 'quick';
// Private sorting strategies
#bubbleSort(arr) {
const result = [...arr];
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result.length - i - 1; j++) {
if (result[j] > result[j + 1]) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
}
}
}
return result;
}
#quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter((x) => x < pivot);
const middle = arr.filter((x) => x === pivot);
const right = arr.filter((x) => x > pivot);
return [...this.#quickSort(left), ...middle, ...this.#quickSort(right)];
}
#mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = this.#mergeSort(arr.slice(0, mid));
const right = this.#mergeSort(arr.slice(mid));
return this.#merge(left, right);
}
#merge(left, right) {
const result = [];
while (left.length && right.length) {
result.push(left[0] < right[0] ? left.shift() : right.shift());
}
return [...result, ...left, ...right];
}
setStrategy(strategy) {
const valid = ['bubble', 'quick', 'merge'];
if (!valid.includes(strategy)) {
throw new Error(`Invalid strategy. Use: ${valid.join(', ')}`);
}
this.#strategy = strategy;
}
sort(arr) {
switch (this.#strategy) {
case 'bubble':
return this.#bubbleSort(arr);
case 'quick':
return this.#quickSort(arr);
case 'merge':
return this.#mergeSort(arr);
default:
return this.#quickSort(arr);
}
}
}
const sorter = new Sorter();
const data = [5, 2, 8, 1, 9, 3];
console.log('Quick sort:', sorter.sort(data));
sorter.setStrategy('merge');
console.log('Merge sort:', sorter.sort(data));
/**
* EXAMPLE 19: Builder Pattern with Private State
* Fluent builder with encapsulated state
*/
console.log('\n--- Example 19: Builder Pattern ---');
class RequestBuilder {
#method = 'GET';
#url = '';
#headers = new Map();
#body = null;
#timeout = 30000;
// Fluent methods
get(url) {
this.#method = 'GET';
this.#url = url;
return this;
}
post(url) {
this.#method = 'POST';
this.#url = url;
return this;
}
put(url) {
this.#method = 'PUT';
this.#url = url;
return this;
}
header(key, value) {
this.#headers.set(key, value);
return this;
}
json(data) {
this.#body = JSON.stringify(data);
this.#headers.set('Content-Type', 'application/json');
return this;
}
timeout(ms) {
this.#timeout = ms;
return this;
}
build() {
return {
method: this.#method,
url: this.#url,
headers: Object.fromEntries(this.#headers),
body: this.#body,
timeout: this.#timeout,
};
}
// Reset builder for reuse
reset() {
this.#method = 'GET';
this.#url = '';
this.#headers.clear();
this.#body = null;
this.#timeout = 30000;
return this;
}
}
const request = new RequestBuilder()
.post('https://api.example.com/users')
.header('Authorization', 'Bearer token123')
.json({ name: 'Alice', email: 'alice@example.com' })
.timeout(5000)
.build();
console.log('Built request:', request);
/**
* EXAMPLE 20: Complex State Machine with Private Internals
* Full state machine implementation
*/
console.log('\n--- Example 20: Complex State Machine ---');
class StateMachine {
#currentState;
#states = new Map();
#transitions = [];
#onEnter = new Map();
#onExit = new Map();
constructor(initialState) {
this.#currentState = initialState;
}
// Private transition finder
#findTransition(event) {
return this.#transitions.find(
(t) => t.from === this.#currentState && t.event === event
);
}
// Private state change handler
#changeState(newState) {
const oldState = this.#currentState;
// Exit handler
const exitHandler = this.#onExit.get(oldState);
if (exitHandler) exitHandler(oldState, newState);
this.#currentState = newState;
// Enter handler
const enterHandler = this.#onEnter.get(newState);
if (enterHandler) enterHandler(newState, oldState);
}
// Public configuration methods
addState(name) {
this.#states.set(name, { name });
return this;
}
addTransition(from, event, to, guard = null) {
this.#transitions.push({ from, event, to, guard });
return this;
}
onEnterState(state, handler) {
this.#onEnter.set(state, handler);
return this;
}
onExitState(state, handler) {
this.#onExit.set(state, handler);
return this;
}
// Public action methods
dispatch(event, data = null) {
const transition = this.#findTransition(event);
if (!transition) {
console.log(`No transition for '${event}' from '${this.#currentState}'`);
return false;
}
// Check guard condition
if (transition.guard && !transition.guard(data)) {
console.log(`Guard prevented transition for '${event}'`);
return false;
}
console.log(
`Transition: ${this.#currentState} --${event}--> ${transition.to}`
);
this.#changeState(transition.to);
return true;
}
get state() {
return this.#currentState;
}
can(event) {
return this.#findTransition(event) !== null;
}
}
// Create an order state machine
const orderMachine = new StateMachine('pending')
.addState('pending')
.addState('confirmed')
.addState('shipped')
.addState('delivered')
.addState('cancelled')
.addTransition('pending', 'confirm', 'confirmed')
.addTransition('pending', 'cancel', 'cancelled')
.addTransition('confirmed', 'ship', 'shipped')
.addTransition('confirmed', 'cancel', 'cancelled')
.addTransition('shipped', 'deliver', 'delivered')
.onEnterState('confirmed', () => console.log('📧 Sending confirmation email'))
.onEnterState('shipped', () => console.log('📦 Generating tracking number'))
.onEnterState('delivered', () => console.log('✅ Order completed!'));
console.log('Initial state:', orderMachine.state);
orderMachine.dispatch('confirm');
orderMachine.dispatch('ship');
orderMachine.dispatch('deliver');
// Try invalid transition
orderMachine.dispatch('cancel'); // No transition from 'delivered'
console.log('\n========================================');
console.log('End of Private Fields Examples');
console.log('========================================');