JavaScript Design Patterns
Apply classic and JavaScript-specific design patterns Observer, Strategy, Command, Decorator, and Module to write maintainable production code.
Overview
Key Points
- •Observer / EventEmitter: objects subscribe to events; emitting an event notifies all subscribers without tight coupling
- •Strategy: encapsulate interchangeable algorithms behind a common interface; swap them without changing the caller
- •Command: wrap a request as an object with execute/undo enables queuing, logging, and undo/redo
- •Decorator: wrap an object to add behaviour without modifying it or creating a subclass
- •Module pattern: use closures (or ES modules) to create private state and expose a public API
- •Factory: a function or method that creates objects, hiding construction details from the caller
- •Singleton: ensure a class or module has only one instance ES modules provide this automatically
- •Builder: construct complex objects step-by-step with a fluent API, returning the result at the end
Examples
Observer / EventEmitter from scratch
class EventEmitter {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(fn);
return () => this.off(event, fn); // returns unsubscribe function
}
off(event, fn) {
this.#listeners.get(event)?.delete(fn);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(fn => fn(...args));
}
once(event, fn) {
const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
return this.on(event, wrapper);
}
}
const store = new EventEmitter();
const unsub = store.on('change', data => console.log('Changed:', data));
store.once('ready', () => console.log('Ready!'));
store.emit('ready'); // Ready!
store.emit('change', 42); // Changed: 42
store.emit('ready'); // (no output once fired once)
unsub();
store.emit('change', 99); // (no output unsubscribed)Returning an unsubscribe function from on() is more ergonomic than requiring a matching off() call with the original function reference.
Strategy pattern pluggable sort algorithms
// Each strategy is just a function no class hierarchy needed
const strategies = {
bubble(arr) {
const a = [...arr];
for (let i = 0; i < a.length; i++)
for (let j = 0; j < a.length - i - 1; j++)
if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]];
return a;
},
quick(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const mid = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
return [...strategies.quick(left), ...mid, ...strategies.quick(right)];
},
};
class Sorter {
constructor(strategy = strategies.quick) {
this.strategy = strategy;
}
setStrategy(strategy) { this.strategy = strategy; }
sort(data) { return this.strategy(data); }
}
const sorter = new Sorter();
console.log(sorter.sort([5, 1, 4, 2, 3])); // [1, 2, 3, 4, 5]
sorter.setStrategy(strategies.bubble);
console.log(sorter.sort([5, 1, 4, 2, 3])); // [1, 2, 3, 4, 5]In JavaScript, strategies are typically plain functions no Strategy interface class needed.
Command pattern with undo stack
class TextEditor {
#text = '';
#history = [];
#future = [];
execute(command) {
command.execute(this);
this.#history.push(command);
this.#future = [];
}
undo() {
const cmd = this.#history.pop();
if (cmd) { cmd.undo(this); this.#future.push(cmd); }
}
redo() {
const cmd = this.#future.pop();
if (cmd) { cmd.execute(this); this.#history.push(cmd); }
}
get text() { return this.#text; }
set text(v) { this.#text = v; }
}
const insertCommand = (chars, position) => ({
execute(editor) {
const t = editor.text;
editor.text = t.slice(0, position) + chars + t.slice(position);
},
undo(editor) {
const t = editor.text;
editor.text = t.slice(0, position) + t.slice(position + chars.length);
}
});
const editor = new TextEditor();
editor.execute(insertCommand('Hello', 0));
editor.execute(insertCommand(' World', 5));
console.log(editor.text); // Hello World
editor.undo();
console.log(editor.text); // Hello
editor.redo();
console.log(editor.text); // Hello WorldCommands as plain objects (not classes) are idiomatic JavaScript the execute/undo interface is enforced by convention.
Common Mistakes
- Implementing Singleton with a global variable instead of a module-level export module-level singletons are automatically lazy and work correctly with bundlers
- Mutating strategy state inside the strategy function strategies should be pure and stateless so they can be swapped freely
- Adding listeners in a tight loop without keeping the unsubscribe reference leads to memory leaks and duplicate notifications
- Making Command objects too granular every character typed should be batched into a word-level command, not a per-keystroke command, for practical undo/redo
Note
Prefer composition over inheritance for applying patterns. JavaScript closures and higher-order functions often let you express a pattern in 5 lines that would require 5 classes in Java. Always ask: "Can I replace this class hierarchy with a function that takes a function?"
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4