Skip to Content
TheoryComposition vs Inheritance

Composition vs Inheritance in JavaScript

Understand the trade-offs between composition and inheritance in JavaScript and React development. This guide walks through practical examples of function composition, class inheritance, and the JavaScript call stack to help you build cleaner, more flexible systems.

composition

Composition vs Inheritance People sometimes say “composition” when contrasting it with inheritance. This has less to do with functions (which we’ve been discussing all along) and more to do with objects and classes — that is, with traditional object-oriented programming.

In particular, if you express your code as classes, it is tempting to reuse behavior from another class by extending it (inheritance). However, this makes it somewhat difficult to adjust the behavior later. For example, you may want to similarly reuse behavior from another class, but you can’t extend more than one base class.

Sometimes, people say that inheritance “locks you into” your first design because the cost of changing the class hierarchy later is too high. When people suggest composition is an alternative to inheritance, they mean that instead of extending a class, you can keep an instance of that class as a field. Then you can “delegate” to that instance when necessary, but you are also free to do something different.

Function composition is a powerful concept, but it raises the level of abstraction and makes your code less direct. If you write your code in a style that composes functions in some way before calling them, and there are other humans on your team, make sure that you’re getting concrete benefits from this approach. It is not “cleaner” or “better”, and there is a price to pay for “beautiful” but indirect code.

composition article

composition
const dateFunc = () => new Date(); const textFunc = (date) => date.toDateString(); const labelFunc = (text) => `Today ${text}`; const showLabelFunc = (label) => console.log(label); const date = dateFunc(); const text = textFunc(date); const label = labelFunc(text); showLabelFunc(label); // Today Sat Sep 28 2024 function pipe(...steps) { return function runSteps() { let result; for (let i = 0; i < steps.length; i++) { let step = steps[i]; result = step(result); } return result; }; } const showDateLabel = pipe(dateFunc, textFunc, labelFunc, showLabelFunc); showDateLabel(); // Today Sat Sep 28 2024

inheritance

inheritance
// Base class class Vehicle { private readonly _make: string; private readonly _model: string; private readonly _year: number; constructor(make: string, model: string, year: number) { this._make = make; this._model = model; this._year = year; } displayInfo(): string { return `${this._year} ${this._make} ${this._model}`; } } // Derived class class Car extends Vehicle { private readonly _doors: number; constructor( make: string, model: string, year: number, doors: number, ) { super(make, model, year); // Call the constructor of the base class this._doors = doors; } displayInfo(): string { return `${super.displayInfo()} - ${this._doors} doors`; } } const vehicle = new Vehicle("Toyota", "Corolla", 2020); console.log(vehicle.displayInfo()); // 2020 Toyota Corolla const car = new Car("Honda", "Civic", 2022, 4); console.log(car.displayInfo()); // 2022 Honda Civic - 4 doors

Function Stack

The call stack is a critical concept in JavaScript that keeps track of function calls. When a function is called, it is “pushed” onto the stack. Once the function finishes execution, it is “popped” off the stack. This stack-based approach is essential for understanding recursion and function execution order in JavaScript.

In the forward phase, the function calls accumulate as the recursion continues, with each new function invocation pushing itself onto the stack. In the backward phase, as functions start to return, they unwind and are popped from the stack, completing their execution.

This behavior is demonstrated in the following example, where foo is called recursively until it hits the base case, then the stack is unwound as each function completes.

Forward Phase (Pushing):

  1. Call foo(2) -> Stack: [foo(2)].
  2. Call foo(1) -> Stack: [foo(2), foo(1)].
  3. Call foo(0) -> Stack: [foo(2), foo(1), foo(0)].
  4. Call foo(-1) -> Stack: [foo(2), foo(1), foo(0), foo(-1)].

Backward Phase (Unwinding):

  1. Return from foo(-1) -> Stack: [foo(2), foo(1), foo(0)].
  2. Complete foo(0) -> Stack: [foo(2), foo(1)].
  3. Complete foo(1) -> Stack: [foo(2)].
  4. Complete foo(2) -> Stack: [].
function stack
function foo(i) { if (i < 0) { return; } console.log(`begin: ${i}`); foo(i - 1); console.log(`end: ${i}`); } foo(2); // begin: 2 // begin: 1 // begin: 0 // end: 0 // end: 1 // end: 2
Last updated on