21 · Object-Oriented Programming Foundations
The OOP paradigm from first principles: objects, classes, the four pillars (encapsulation, abstraction, inheritance, polymorphism), how JavaScript actually does objects (prototypes vs
classsyntax), composition vs inheritance, and a tour of the design patterns that recur everywhere. Written from near-zero, but reaching the depth a senior is expected to discuss.
Positioning
OOP is one of the two paradigms a frontend engineer must understand (the other is functional, 22). Even in a function-heavy React world, OOP concepts are everywhere: the DOM is an object tree, class components and many libraries are OO, design patterns (Observer, Strategy, Factory) describe code you write daily, and SOLID (20) is OO design guidance. You don’t need to write deep hierarchies — modern frontend favors composition — but you must understand OOP to read code, use libraries, design APIs, and pass interviews.
Foundations: what “object-oriented” means
Start from the basics you know — variables, functions, loops. OOP adds one idea: bundle related data and the behavior that operates on it into a single unit called an object. Instead of free-floating data and functions, an object owns its data (fields/properties/state) and exposes behavior (methods) that act on that data.
- Object — a concrete thing with state + behavior.
{ balance: 100, deposit(x) {...} }. - Class — a blueprint for creating objects of a kind.
class Account { ... };new Account()makes an instance. - Message passing — you interact with an object by calling its methods (Alan Kay’s original framing of OOP was about objects sending messages, more than about classes).
The promise: model the problem domain as collaborating objects that mirror real-world concepts (Order, User, Cart), making large systems comprehensible.
Deep dive: the four pillars
1. Encapsulation
Bundle state with behavior and hide the internals, exposing a controlled public interface. Callers use methods; they can’t reach in and corrupt private state. This enforces invariants (an Account never goes negative because only withdraw() can change the balance, and it checks).
- In JS:
#privatefields (#balance), closures, or convention (_private). TypeScript addsprivate/protected/readonly(compile-time). - Benefit: you can change internals freely as long as the public contract holds (this is the Open/Closed and information-hiding payoff).
2. Abstraction
Expose what something does, hide how. A List.sort() caller doesn’t care which algorithm runs. Abstraction reduces cognitive load and decouples callers from implementation. Interfaces/abstract types name a capability without committing to an implementation — the basis of Dependency Inversion (20) and ports/adapters (10).
3. Inheritance
Define a new class as a specialization of an existing one, reusing and extending its behavior. class SavingsAccount extends Account. The subclass is-a superclass.
- Provides reuse and polymorphism, but couples the child to the parent’s internals and is rigid. Deep hierarchies are fragile (a change high up ripples down — the “fragile base class” problem).
- Liskov Substitution (
20) constrains it: a subclass must be usable wherever the base is. IfSquare extends Rectanglebut breakssetWidth/setHeightindependence, the model is wrong. - Modern guidance: prefer composition (below); use inheritance sparingly, for genuine is-a relationships with stable bases.
4. Polymorphism
One interface, many implementations — the same call does the right thing for the actual object type. shapes.forEach(s => s.area()) works whether s is a Circle or Square. This is what lets you write code against an abstraction and plug in new types without changing the caller (Open/Closed).
- Subtype polymorphism (override methods), parametric polymorphism (generics,
03), ad-hoc polymorphism (overloading). “Duck typing” (JS/structural typing,03) is polymorphism by shape, not by declared type.
How JavaScript actually does objects
This trips up many engineers, so it’s worth being precise (02 goes deeper).
Prototypes are the real mechanism
JS is prototype-based, not class-based underneath. Every object has an internal link ([[Prototype]], exposed via Object.getPrototypeOf) to another object. Property/method lookups walk this prototype chain until found or null. “Inheritance” in JS is delegation along this chain.
const animal = { speak() { return `${this.name} makes a sound`; } };
const dog = Object.create(animal); // dog's prototype IS animal
dog.name = 'Rex';
dog.speak(); // found on prototype → "Rex makes a sound"
class is syntactic sugar over prototypes
ES6 class is a cleaner syntax for the prototype pattern — not a new object model.
class Account {
#balance = 0; // private field (encapsulation)
constructor(initial) { this.#balance = initial; }
deposit(x) { this.#balance += x; return this; }
get balance() { return this.#balance; }
}
class Savings extends Account { // sets up the prototype chain
constructor(initial, rate) { super(initial); this.rate = rate; }
addInterest() { this.deposit(this.balance * this.rate); } // polymorphic reuse
}
Methods live on Account.prototype; super calls up the chain; #balance is truly private. Know that this binding (02) is the classic OOP-in-JS footgun — a detached method loses this unless bound or an arrow.
this, bind, and the footguns
this is determined by how a function is called, not where it’s defined. Method extracted into a callback loses its receiver. Fixes: arrow functions (lexical this), .bind(this), or class fields holding arrows. This is why class React components needed bind in constructors.
Composition over inheritance (the modern default)
Instead of extends, build objects that hold other objects/functions and delegate to them. “Has-a” beats “is-a.”
// Inheritance (rigid): class FlyingDuck extends Duck extends Bird ...
// Composition (flexible): assemble behaviors
const canFly = (s) => ({ fly: () => `${s.name} flies` });
const canSwim = (s) => ({ swim: () => `${s.name} swims` });
function makeDuck(name) {
const self = { name };
return Object.assign(self, canFly(self), canSwim(self)); // mix capabilities
}
Benefits: no fragile hierarchy, mix-and-match behaviors, easier testing. React embodies this — components compose components, logic composes via hooks (05), not via inheritance (React has no component inheritance by design).
Design patterns (the reusable solutions)
The Gang of Four (GoF) catalog names recurring solutions. You don’t memorize all 23, but seniors recognize the common ones. Three categories:
Creational (how objects are made)
- Factory / Factory Method — a function/method that creates objects, hiding the concrete type (
createStore(),createClient()). - Singleton — one shared instance (a config, a store). Often an anti-pattern (global state, hard to test) — use sparingly.
- Builder — construct complex objects step by step (fluent APIs, query builders).
Structural (how objects are composed)
- Adapter — wrap an incompatible interface to fit an expected one (the “A” in ports-and-adapters,
10). - Decorator — wrap an object to add behavior without changing it (HOCs, middleware, React’s wrapping components).
- Facade — a simple interface over a complex subsystem (a service module hiding several APIs; a BFF,
12). - Proxy — stand in for another object to control access (Valtio/Vue reactivity use JS
Proxy,06).
Behavioral (how objects interact)
- Observer / Pub-Sub — subjects notify subscribers of changes. The pattern behind reactivity, event buses,
useSyncExternalStore, signals (05,06), and DOM events (04). - Strategy — swap interchangeable algorithms behind a common interface (a pluggable sort/validation/format).
- Command — encapsulate an action as an object (undo/redo, Redux actions, queues).
- State — behavior changes with internal state (statecharts/XState,
06). - Iterator — sequential access without exposing structure (JS iterators/
for...of,02).
Patterns are vocabulary and guidance, not goals — applying patterns you don’t need is over-engineering (KISS/YAGNI, 20).
Worked example: Strategy + Observer in plain JS
// Strategy: interchangeable shipping-cost algorithms behind one interface
const strategies = {
standard: (w) => w * 2,
express: (w) => w * 5 + 10,
free: () => 0,
};
function shippingCost(method, weight) { return strategies[method](weight); } // open for new strategies
// Observer: a tiny subject others subscribe to (the heart of reactivity)
function createSubject() {
const listeners = new Set();
return {
subscribe: (fn) => (listeners.add(fn), () => listeners.delete(fn)),
notify: (value) => listeners.forEach((fn) => fn(value)),
};
}
const cart = createSubject();
const unsub = cart.subscribe((items) => render(items)); // UI reacts to changes
cart.notify([{ id: 1 }]); // push update
Strategy gives Open/Closed extensibility; Observer is the mechanism every state library (06) generalizes.
Pitfalls & gotchas
- Deep inheritance hierarchies → fragile base class, rigidity; prefer composition.
thisbinding bugs → extracted methods lose their receiver; use arrows/bind.- Misunderstanding
classas a new model → it’s prototypes underneath; surprises withprototype,instanceof, mutation of shared prototypes. - Singletons as global state → hidden coupling, test pain.
- LSP violations → subclasses that break the base contract (Square/Rectangle).
- Pattern-itis → forcing GoF patterns where a plain function would do (KISS/YAGNI).
- Mutable shared objects → aliasing bugs (recall pass-by-reference for objects in JS); encapsulation + immutability (
22) guard against it. - Anemic objects → data bags with no behavior; logic leaks into “services” everywhere (sometimes fine on the frontend, but name the tradeoff).
Interview questions
- What are the four pillars of OOP? Define each with an example.
- How does JavaScript implement objects and “inheritance” under the hood?
classvs prototype — what’s really happening?- Why prefer composition over inheritance? Give a case where inheritance is still right.
- Explain polymorphism and how it enables Open/Closed.
- What is the Liskov Substitution Principle and a violation of it?
- How does
thisget bound, and what breaks it? - Explain the Observer pattern and where it shows up in frontend.
- Adapter vs Decorator vs Facade vs Proxy — distinguish them.
- When is using a design pattern a mistake?
Recommendations
- Understand OOP deeply; write composition-first code (functions, hooks, has-a) and reserve inheritance for genuine, shallow is-a with stable bases.
- Use encapsulation (private fields, modules) to protect invariants; expose minimal interfaces (ISP).
- Lean on polymorphism/abstraction for Open/Closed extensibility (strategy maps, ports).
- Know the common patterns as vocabulary; apply only when they remove real complexity.
- In JS, respect
thissemantics and prototype mechanics; prefer immutability to dodge shared-mutation bugs (22).
Books & references
- “Head First Design Patterns” (2nd ed) — Freeman & Robson. The most approachable patterns book; OO principles taught through examples. Best starting point.
- “Design Patterns: Elements of Reusable OO Software” — Gamma, Helm, Johnson, Vlissides (GoF). The original catalog; reference, not a tutorial.
- “Patterns.dev” — Lydia Hallie & Addy Osmani (free at patterns.dev). Patterns specifically for modern JS/React — the best frontend-flavored resource.
- “Object-Oriented Software Construction” — Bertrand Meyer; “Refactoring” — Fowler (smells → patterns).
- MDN — “Object prototypes” & “Classes” — the authoritative JS object-model reference.
- “You Don’t Know JS: this & Object Prototypes” — Kyle Simpson (free). The clearest deep dive on JS’s real object model.
- (Counterpoint reading) — Sandi Metz, “Nothing is Something” and POOD; A Philosophy of Software Design (
20) on when OO advice misfires.
Connections
22-functional-programming.md— the other paradigm; modern frontend blends both (OO boundaries, FP cores).20-programming-principles.md— SOLID, composition-over-inheritance, and patterns are OO design guidance.02-javascript-deep-dive.md— prototypes,this, classes, iterators in mechanical detail.05-react-internals-and-patterns.md— composition, HOCs (Decorator), Observer (reactivity), no inheritance.06-state-management-and-stores.md— Observer/Proxy/Command/State patterns generalized into stores.10-frontend-architecture.md— Adapter/Facade as ports-and-adapters; abstraction as DIP.03-typescript.md— interfaces, generics (parametric polymorphism), access modifiers.