DEV Community

Cover image for Functional Programming vs Object-Oriented Programming in JavaScript
Abanoub Kerols
Abanoub Kerols

Posted on

Functional Programming vs Object-Oriented Programming in JavaScript

A Practical Guide to FP, OOP, Prototypes, Composition, Currying, Immutability, and Encapsulation

Modern JavaScript supports multiple programming paradigms. Two of the most important are:

  • Object-Oriented Programming (OOP)
  • Functional Programming (FP)

JavaScript is particularly interesting because it is a multi-paradigm language. You can build applications using classes and inheritance, functional composition and higher-order functions, or combine both approaches.

Understanding these paradigms is more important than simply knowing their syntax. The real skill is knowing how the underlying concepts affect architecture, state management, testability, reusability, and maintainability.

This article explores the major concepts behind both paradigms and shows how they work internally in JavaScript.


Table of Contents

  1. Programming Paradigms
  2. Functional Programming
  3. Object-Oriented Programming
  4. Abstract Classes and Inheritance
  5. Prototypes and Constructors
  6. Composition Over Inheritance
  7. Functional Composition
  8. Factory Functions
  9. Currying
  10. Partial Application
  11. Higher-Order Functions
  12. Lambda Functions
  13. Recursive Functions
  14. Pure Functions
  15. Immutable vs Mutable State
  16. Private Properties and Methods
  17. FP vs OOP
  18. Using FP and OOP Together
  19. Real-World Architecture Example
  20. Best Practices
  21. Conclusion

1. Programming Paradigms

A programming paradigm is a way of thinking about how software should be structured.

Instead of asking:

"What syntax should I use?"

we should ask:

"How should I model the problem?"

Two major approaches are:

Programming
│
├── Imperative
│   ├── Procedural
│   └── Object-Oriented
│
└── Declarative
    └── Functional
Enter fullscreen mode Exit fullscreen mode

Consider a simple requirement:

Calculate the total price of products.

An imperative approach might say:

let total = 0;

for (const product of products) {
    total += product.price;
}
Enter fullscreen mode Exit fullscreen mode

A functional approach describes the transformation:

const total = products.reduce(
    (sum, product) => sum + product.price,
    0
);
Enter fullscreen mode Exit fullscreen mode

The difference is not just syntax.

It is about how we reason about the program.


2. Functional Programming

Functional Programming treats computation primarily as the evaluation of functions.

The central idea is:

Build programs by composing small functions that transform data.

For example:

const addTax = price => price * 1.14;

const addShipping = price => price + 100;

const calculateOrderTotal = price =>
    addShipping(addTax(price));

console.log(calculateOrderTotal(1000));
Enter fullscreen mode Exit fullscreen mode

Each function performs one transformation.

Conceptually:

price
  ↓
addTax
  ↓
addShipping
  ↓
result
Enter fullscreen mode Exit fullscreen mode

Functional programming emphasizes:

  • Pure functions
  • Immutability
  • Function composition
  • Higher-order functions
  • Referential transparency
  • Declarative programming
  • Avoiding shared mutable state

3. Object-Oriented Programming

Object-Oriented Programming organizes software around objects.

An object combines:

State + Behavior
Enter fullscreen mode Exit fullscreen mode

For example:

class BankAccount {
    constructor(balance) {
        this.balance = balance;
    }

    deposit(amount) {
        this.balance += amount;
    }

    withdraw(amount) {
        if (amount > this.balance) {
            throw new Error("Insufficient funds");
        }

        this.balance -= amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const account = new BankAccount(1000);

account.deposit(500);
account.withdraw(200);

console.log(account.balance);
Enter fullscreen mode Exit fullscreen mode

The object owns both:

State
  ↓
balance

Behavior
  ↓
deposit()
withdraw()
Enter fullscreen mode Exit fullscreen mode

OOP commonly uses:

  • Encapsulation
  • Abstraction
  • Inheritance
  • Polymorphism
  • Composition

4. Abstract Classes and Inheritance

Abstraction means exposing what an object can do while hiding implementation details.

For example:

class PaymentProcessor {
    processPayment(amount) {
        throw new Error("processPayment() must be implemented");
    }
}
Enter fullscreen mode Exit fullscreen mode

Then:

class StripePayment extends PaymentProcessor {
    processPayment(amount) {
        console.log(`Processing ${amount} using Stripe`);
    }
}
Enter fullscreen mode Exit fullscreen mode

And:

class PayPalPayment extends PaymentProcessor {
    processPayment(amount) {
        console.log(`Processing ${amount} using PayPal`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can work with the abstraction:

function checkout(processor, amount) {
    processor.processPayment(amount);
}
Enter fullscreen mode Exit fullscreen mode

Usage:

checkout(new StripePayment(), 500);
checkout(new PayPalPayment(), 500);
Enter fullscreen mode Exit fullscreen mode

This demonstrates polymorphism.

The caller doesn't need to know the concrete implementation.


Important JavaScript Difference

Unlike Java or C#, JavaScript does not have a traditional abstract class keyword.

We usually simulate abstraction using:

class PaymentProcessor {
    processPayment() {
        throw new Error("Not implemented");
    }
}
Enter fullscreen mode Exit fullscreen mode

Or we can use TypeScript:

abstract class PaymentProcessor {
    abstract processPayment(amount: number): void;
}

class StripePayment extends PaymentProcessor {
    processPayment(amount: number) {
        console.log(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Prototypes and Constructors

JavaScript is fundamentally prototype-based.

Classes are largely syntactic sugar over JavaScript's prototype system.

Consider:

function User(name) {
    this.name = name;
}

User.prototype.sayHello = function () {
    console.log(`Hello ${this.name}`);
};
Enter fullscreen mode Exit fullscreen mode

Now:

const user1 = new User("John");
const user2 = new User("Alice");
Enter fullscreen mode Exit fullscreen mode

Both objects can access:

user1.sayHello();
user2.sayHello();
Enter fullscreen mode Exit fullscreen mode

But the method does not need to be copied into every object.

Instead:

user1
   │
   ▼
User.prototype
   │
   └── sayHello()

user2
   │
   ▼
User.prototype
   │
   └── sayHello()
Enter fullscreen mode Exit fullscreen mode

This is the prototype chain.


What Does new Do?

When we write:

const user = new User("John");
Enter fullscreen mode Exit fullscreen mode

JavaScript roughly performs these steps:

1. Create a new object
2. Link object to User.prototype
3. Execute User with `this`
4. Return the object
Enter fullscreen mode Exit fullscreen mode

Conceptually:

const user = Object.create(User.prototype);

User.call(user, "John");
Enter fullscreen mode Exit fullscreen mode

This is not a complete specification-level implementation, but it provides the right mental model.


6. Composition Over Inheritance

Inheritance can become problematic when hierarchies become deep.

Imagine:

Animal
  ↓
Mammal
  ↓
FlyingMammal
  ↓
FlyingSwimmingMammal
Enter fullscreen mode Exit fullscreen mode

As requirements grow, the hierarchy becomes difficult to maintain.

Composition takes another approach.

Instead of asking:

"What is this object?"

we ask:

"What capabilities does this object have?"

For example:

const canWalk = state => ({
    walk() {
        console.log(`${state.name} is walking`);
    }
});

const canFly = state => ({
    fly() {
        console.log(`${state.name} is flying`);
    }
});

const canSwim = state => ({
    swim() {
        console.log(`${state.name} is swimming`);
    }
});
Enter fullscreen mode Exit fullscreen mode

Now:

const bird = {
    name: "Eagle",
    ...canWalk({ name: "Eagle" }),
    ...canFly({ name: "Eagle" })
};
Enter fullscreen mode Exit fullscreen mode

And:

const duck = {
    name: "Duck",
    ...canWalk({ name: "Duck" }),
    ...canFly({ name: "Duck" }),
    ...canSwim({ name: "Duck" })
};
Enter fullscreen mode Exit fullscreen mode

Instead of building a large inheritance tree, we compose capabilities.


7. Functional Composition

Functional composition means combining functions to create a new function.

Suppose we have:

const double = x => x * 2;

const square = x => x * x;
Enter fullscreen mode Exit fullscreen mode

We can compose them:

const doubleThenSquare = x =>
    square(double(x));
Enter fullscreen mode Exit fullscreen mode

Then:

doubleThenSquare(3);
Enter fullscreen mode Exit fullscreen mode

Execution:

3
 ↓
double
 ↓
6
 ↓
square
 ↓
36
Enter fullscreen mode Exit fullscreen mode

Building a compose Function

We can generalize the concept:

const compose = (...functions) =>
    value =>
        functions.reduceRight(
            (result, fn) => fn(result),
            value
        );
Enter fullscreen mode Exit fullscreen mode

Now:

const doubleThenSquare = compose(
    square,
    double
);

console.log(doubleThenSquare(3));
Enter fullscreen mode Exit fullscreen mode

Result:

36
Enter fullscreen mode Exit fullscreen mode

We can also create pipe, which executes from left to right:

const pipe = (...functions) =>
    value =>
        functions.reduce(
            (result, fn) => fn(result),
            value
        );
Enter fullscreen mode Exit fullscreen mode

Then:

const processPrice = pipe(
    price => price * 1.14,
    price => price + 100,
    price => Math.round(price)
);
Enter fullscreen mode Exit fullscreen mode

Usage:

processPrice(1000);
Enter fullscreen mode Exit fullscreen mode

This style is very common in functional JavaScript.


8. Factory Functions

A factory function is a function that creates objects.

Instead of:

class User {
    constructor(name) {
        this.name = name;
    }

    greet() {
        return `Hello ${this.name}`;
    }
}
Enter fullscreen mode Exit fullscreen mode

we can use:

function createUser(name) {
    return {
        name,

        greet() {
            return `Hello ${this.name}`;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const user = createUser("John");

console.log(user.greet());
Enter fullscreen mode Exit fullscreen mode

Factory functions are particularly useful when combined with composition.


Factory + Composition

const createUser = name => ({
    name
});

const withAuthentication = user => ({
    ...user,

    login() {
        console.log(`${user.name} logged in`);
    }
});

const withPermissions = user => ({
    ...user,

    hasPermission(permission) {
        return permission === "admin";
    }
});
Enter fullscreen mode Exit fullscreen mode

Now:

const admin = withPermissions(
    withAuthentication(
        createUser("John")
    )
);
Enter fullscreen mode Exit fullscreen mode

This gives us a composable object.


9. Currying

Currying transforms a function that takes multiple arguments into a sequence of functions that each take one argument.

Normal function:

const add = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

Curried version:

const add = a => b => a + b;
Enter fullscreen mode Exit fullscreen mode

Usage:

add(10)(20);
Enter fullscreen mode Exit fullscreen mode

Result:

30
Enter fullscreen mode Exit fullscreen mode

Why Is Currying Useful?

Because it allows us to specialize functions.

For example:

const multiply = a => b => a * b;

const double = multiply(2);
const triple = multiply(3);
Enter fullscreen mode Exit fullscreen mode

Now:

double(10); // 20
triple(10); // 30
Enter fullscreen mode Exit fullscreen mode

We created specialized functions from a general function.


10. Partial Application

Currying and partial application are related but not identical.

Partial application means:

Fix some arguments now and provide the rest later.

Consider:

const createUser = (role, name, age) => ({
    role,
    name,
    age
});
Enter fullscreen mode Exit fullscreen mode

We can specialize the role:

const createAdmin = (name, age) =>
    createUser("admin", name, age);
Enter fullscreen mode Exit fullscreen mode

Now:

createAdmin("John", 30);
Enter fullscreen mode Exit fullscreen mode

Conceptually:

createUser
   │
   ├── role = admin
   │
   ▼
createAdmin
Enter fullscreen mode Exit fullscreen mode

Partial application is useful for creating reusable domain-specific functions.


11. Higher-Order Functions

A higher-order function is a function that:

  1. Receives another function as an argument
  2. Returns a function
  3. Or both

For example:

function executeOperation(a, b, operation) {
    return operation(a, b);
}
Enter fullscreen mode Exit fullscreen mode

Usage:

executeOperation(10, 20, (a, b) => a + b);
Enter fullscreen mode Exit fullscreen mode

Result:

30
Enter fullscreen mode Exit fullscreen mode

JavaScript's array methods are excellent examples:

map()
filter()
reduce()
some()
every()
find()
Enter fullscreen mode Exit fullscreen mode

For example:

const users = [
    { name: "John", active: true },
    { name: "Alice", active: false },
    { name: "Bob", active: true }
];

const activeUsers = users.filter(
    user => user.active
);
Enter fullscreen mode Exit fullscreen mode

filter is a higher-order function because it receives a function.


12. Lambda Functions

In JavaScript, lambda functions are generally represented by arrow functions.

Example:

const square = x => x * x;
Enter fullscreen mode Exit fullscreen mode

Compared to:

function square(x) {
    return x * x;
}
Enter fullscreen mode Exit fullscreen mode

Arrow functions provide concise syntax.

They are especially useful for callbacks:

users.map(user => user.name);
Enter fullscreen mode Exit fullscreen mode

Important: Arrow Functions and this

Arrow functions do not create their own this.

Example:

const user = {
    name: "John",

    regular() {
        console.log(this.name);
    },

    arrow: () => {
        console.log(this.name);
    }
};
Enter fullscreen mode Exit fullscreen mode

The regular method receives this based on how it is called.

The arrow function captures this lexically from the surrounding scope.

This difference becomes extremely important in JavaScript applications.


13. Recursive Functions

A recursive function calls itself.

The classic example is factorial:

function factorial(n) {
    if (n === 0) {
        return 1;
    }

    return n * factorial(n - 1);
}
Enter fullscreen mode Exit fullscreen mode

Execution:

factorial(4)

4 × factorial(3)

4 × 3 × factorial(2)

4 × 3 × 2 × factorial(1)

4 × 3 × 2 × 1

24
Enter fullscreen mode Exit fullscreen mode

The critical part is the base case:

if (n === 0) {
    return 1;
}
Enter fullscreen mode Exit fullscreen mode

Without a base case, recursion can continue indefinitely until the call stack is exhausted.


Recursive Tree Traversal

Recursion becomes especially useful when working with hierarchical data.

const tree = {
    value: 1,

    children: [
        {
            value: 2,
            children: []
        },
        {
            value: 3,
            children: [
                {
                    value: 4,
                    children: []
                }
            ]
        }
    ]
};
Enter fullscreen mode Exit fullscreen mode

We can traverse it recursively:

function traverse(node) {
    console.log(node.value);

    for (const child of node.children) {
        traverse(child);
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern appears in:

  • File systems
  • DOM trees
  • ASTs
  • Organizational structures
  • Dependency graphs
  • Nested menus

14. Pure Functions

A pure function has two important properties:

1. Same input → same output

const add = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

For:

add(2, 3);
Enter fullscreen mode Exit fullscreen mode

the result will always be:

5
Enter fullscreen mode Exit fullscreen mode

2. No side effects

A pure function doesn't modify external state.

Impure:

let total = 0;

function add(amount) {
    total += amount;
}
Enter fullscreen mode Exit fullscreen mode

The function changes external state.

Pure:

function add(total, amount) {
    return total + amount;
}
Enter fullscreen mode Exit fullscreen mode

Now the state is explicit.


Why Pure Functions Matter

Pure functions are easier to:

  • Test
  • Debug
  • Cache
  • Reason about
  • Parallelize
  • Reuse

For example:

expect(add(10, 20)).toBe(30);
Enter fullscreen mode Exit fullscreen mode

There is no dependency on external state.


15. Immutable vs Mutable State

Mutable state means changing existing data.

const user = {
    name: "John",
    age: 30
};

user.age = 31;
Enter fullscreen mode Exit fullscreen mode

The same object was modified.

With immutable updates:

const updatedUser = {
    ...user,
    age: 31
};
Enter fullscreen mode Exit fullscreen mode

Now:

user
 ↓
original object

updatedUser
 ↓
new object
Enter fullscreen mode Exit fullscreen mode

The original remains unchanged.


Why Immutability Matters

Immutability makes state transitions easier to reason about.

Consider:

const state1 = {
    count: 0
};

const state2 = {
    ...state1,
    count: state1.count + 1
};

const state3 = {
    ...state2,
    count: state2.count + 1
};
Enter fullscreen mode Exit fullscreen mode

We now have:

state1 → state2 → state3
Enter fullscreen mode Exit fullscreen mode

This idea is fundamental to state-management systems.

For example, reducer-style state updates:

function reducer(state, action) {
    switch (action.type) {
        case "INCREMENT":
            return {
                ...state,
                count: state.count + 1
            };

        default:
            return state;
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of modifying state directly, the reducer returns a new state.


Mutable State Isn't Always Bad

It is important not to turn immutability into a dogma.

Mutation can sometimes be more efficient.

For example:

const array = [];

for (let i = 0; i < 1_000_000; i++) {
    array.push(i);
}
Enter fullscreen mode Exit fullscreen mode

Repeatedly creating new arrays could introduce unnecessary allocations.

The important question is:

Where is mutation allowed, and who owns it?

A practical architecture may use:

Immutable at boundaries
        ↓
Controlled mutation internally
        ↓
Immutable result
Enter fullscreen mode Exit fullscreen mode

This gives you predictable APIs without forcing every internal operation to allocate new objects.


16. Private Properties and Methods

Encapsulation means hiding internal implementation details.

Modern JavaScript supports private class fields using #.

class BankAccount {
    #balance;

    constructor(balance) {
        this.#balance = balance;
    }

    deposit(amount) {
        this.#balance += amount;
    }

    getBalance() {
        return this.#balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

const account = new BankAccount(1000);

account.deposit(500);

console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode

But:

account.#balance;
Enter fullscreen mode Exit fullscreen mode

is invalid.

The property is truly private.


Private Methods

JavaScript also supports private methods:

class UserService {
    #validate(user) {
        if (!user.email) {
            throw new Error("Email is required");
        }
    }

    createUser(user) {
        this.#validate(user);

        console.log("User created");
    }
}
Enter fullscreen mode Exit fullscreen mode

The caller can use:

service.createUser(user);
Enter fullscreen mode Exit fullscreen mode

but cannot directly call:

service.#validate(user);
Enter fullscreen mode Exit fullscreen mode

17. FP vs OOP

Let's compare the mental models.

Concept Functional Programming Object-Oriented Programming
Main abstraction Functions Objects
Primary focus Data transformations State + behavior
State Prefer immutable Often encapsulated/mutable
Reuse Composition Inheritance/composition
Side effects Minimized Encapsulated
Polymorphism Higher-order functions / protocols Inheritance / interfaces
Data flow Explicit Often object-centered
Testing Pure functions are easy Objects require state setup
Best for Transformations Stateful domains

But this table should not lead to:

"FP is better."

or:

"OOP is better."

The better question is:

"Which model fits this problem?"


18. Using FP and OOP Together

Real-world JavaScript applications frequently combine both paradigms.

For example:

class UserService {
    constructor(repository) {
        this.repository = repository;
    }

    async getActiveUsers() {
        const users = await this.repository.findAll();

        return users
            .filter(user => user.active)
            .map(user => ({
                id: user.id,
                name: user.name
            }));
    }
}
Enter fullscreen mode Exit fullscreen mode

The service uses OOP:

class UserService
Enter fullscreen mode Exit fullscreen mode

and functional programming internally:

.filter(...)
.map(...)
Enter fullscreen mode Exit fullscreen mode

This is often a very practical approach.


19. Real-World Architecture Example

Consider an e-commerce system.

We might have:

OrderService
     │
     ├── Repository
     │
     ├── PaymentService
     │
     └── NotificationService
Enter fullscreen mode Exit fullscreen mode

OOP can model long-lived services:

class OrderService {
    constructor(orderRepository, paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    async createOrder(order) {
        // orchestration
    }
}
Enter fullscreen mode Exit fullscreen mode

FP can handle transformations:

const calculateSubtotal = items =>
    items.reduce(
        (total, item) =>
            total + item.price * item.quantity,
        0
    );
Enter fullscreen mode Exit fullscreen mode

Tax:

const calculateTax = rate => subtotal =>
    subtotal * rate;
Enter fullscreen mode Exit fullscreen mode

Discount:

const applyDiscount = discount => total =>
    total - discount;
Enter fullscreen mode Exit fullscreen mode

Then compose the calculation:

const calculateTotal = (items, taxRate, discount) => {
    const subtotal = calculateSubtotal(items);

    const tax = calculateTax(taxRate)(subtotal);

    return applyDiscount(discount)(
        subtotal + tax
    );
};
Enter fullscreen mode Exit fullscreen mode

The architecture becomes:

        OOP
         │
         ▼
   Application Services
         │
         ▼
       FP
         │
         ▼
 Pure Domain Logic
         │
         ▼
       Data
Enter fullscreen mode Exit fullscreen mode

This separation can be extremely powerful.


20. A More Advanced Example

Suppose we need to process orders.

Raw data:

const orders = [
    {
        id: 1,
        status: "completed",
        total: 500
    },
    {
        id: 2,
        status: "pending",
        total: 300
    },
    {
        id: 3,
        status: "completed",
        total: 700
    }
];
Enter fullscreen mode Exit fullscreen mode

We can build small pure functions:

const isCompleted = order =>
    order.status === "completed";

const getTotal = order =>
    order.total;

const sum = (a, b) =>
    a + b;
Enter fullscreen mode Exit fullscreen mode

Then:

const revenue = orders
    .filter(isCompleted)
    .map(getTotal)
    .reduce(sum, 0);
Enter fullscreen mode Exit fullscreen mode

Result:

1200
Enter fullscreen mode Exit fullscreen mode

Each function has one responsibility.

This makes the code easy to test:

isCompleted(orders[0]); // true

getTotal(orders[0]); // 500

sum(500, 700); // 1200
Enter fullscreen mode Exit fullscreen mode

21. A Practical Decision Framework

Instead of choosing FP or OOP based on preference, consider the problem.

Use Functional Programming heavily when:

  • Data transformation is the main problem.
  • You have pipelines of operations.
  • You need predictable business rules.
  • You want highly testable logic.
  • You want to minimize shared state.

Examples:

Data processing
Validation
Calculations
Reducers
Transformations
Parsing
Mapping
Filtering
Aggregation
Enter fullscreen mode Exit fullscreen mode

Use OOP heavily when:

  • You have long-lived entities.
  • Objects own state.
  • You need encapsulation.
  • You have complex domain behavior.
  • You need dependency injection.
  • You are modeling external resources/services.

Examples:

PaymentService
DatabaseRepository
FileStorage
ConnectionPool
Order
UserSession
Cache
Enter fullscreen mode Exit fullscreen mode

Use Composition when:

You have reusable capabilities.

Instead of:

BaseUser
  ↓
AdminUser
  ↓
SuperAdminUser
Enter fullscreen mode Exit fullscreen mode

consider:

User
 + Authentication
 + Authorization
 + Logging
 + Auditing
Enter fullscreen mode Exit fullscreen mode

22. The Deeper Idea: Data vs Behavior

One of the biggest philosophical differences between FP and OOP is how they organize behavior.

OOP tends to ask:

"Who owns this behavior?"

For example:

order.calculateTotal();
Enter fullscreen mode Exit fullscreen mode

The behavior belongs to order.

FP tends to ask:

"What transformation should happen to this data?"

For example:

calculateTotal(order);
Enter fullscreen mode Exit fullscreen mode

Neither approach is universally correct.

They represent two different ways of modeling the same problem.


23. Referential Transparency

A function is referentially transparent when we can replace the function call with its result without changing program behavior.

For example:

const square = x => x * x;
Enter fullscreen mode Exit fullscreen mode

Then:

square(5)
Enter fullscreen mode Exit fullscreen mode

can always be replaced with:

25
Enter fullscreen mode Exit fullscreen mode

Consider:

const result = square(5) + square(5);
Enter fullscreen mode Exit fullscreen mode

We can reason about it as:

const result = 25 + 25;
Enter fullscreen mode Exit fullscreen mode

This property makes functional code easier to reason about.

Compare that with:

let counter = 0;

function increment() {
    return ++counter;
}
Enter fullscreen mode Exit fullscreen mode

Calling:

increment();
Enter fullscreen mode Exit fullscreen mode

cannot be replaced with a fixed value because the result depends on external state.


24. Closures and Functional Programming

Closures are another fundamental JavaScript feature.

Consider:

function createCounter() {
    let count = 0;

    return function () {
        count++;

        return count;
    };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const counter = createCounter();

counter(); // 1
counter(); // 2
counter(); // 3
Enter fullscreen mode Exit fullscreen mode

The returned function remembers:

count
Enter fullscreen mode Exit fullscreen mode

even after createCounter() has finished.

This creates private state without using a class.


25. Factory Functions + Closures

This allows us to build encapsulated objects using FP techniques.

function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit(amount) {
            balance += amount;
        },

        withdraw(amount) {
            if (amount > balance) {
                throw new Error("Insufficient funds");
            }

            balance -= amount;
        },

        getBalance() {
            return balance;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Now:

const account = createBankAccount(1000);

account.deposit(500);

console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode

But:

account.balance;
Enter fullscreen mode Exit fullscreen mode

is:

undefined
Enter fullscreen mode Exit fullscreen mode

because balance exists only inside the closure.

This is an alternative form of encapsulation.


26. OOP Encapsulation vs Closure Encapsulation

OOP:

class BankAccount {
    #balance;

    constructor(balance) {
        this.#balance = balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Functional:

function createBankAccount(balance) {
    return {
        getBalance() {
            return balance;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Both can provide encapsulation.

The difference is the underlying model.

OOP
Object
 ├── State
 └── Methods

FP
Closure
 ├── Captured State
 └── Functions
Enter fullscreen mode Exit fullscreen mode

27. A Production-Oriented Hybrid Design

A mature JavaScript application might look like:

┌──────────────────────────────┐
│        Controllers           │
└──────────────┬───────────────┘
               │
┌──────────────▼───────────────┐
│      Application Services    │
│             OOP              │
└──────────────┬───────────────┘
               │
┌──────────────▼───────────────┐
│       Domain Functions       │
│             FP               │
│                              │
│ Pure calculations            │
│ Validation                   │
│ Transformations              │
└──────────────┬───────────────┘
               │
┌──────────────▼───────────────┐
│      Infrastructure          │
│                              │
│ Database                     │
│ Redis                        │
│ APIs                         │
└──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The important architectural idea is:

Keep business logic as pure as possible and isolate side effects.

For example:

const calculateOrderTotal = order => {
    // pure
};

class OrderService {
    constructor(repository) {
        this.repository = repository;
    }

    async createOrder(order) {
        const total = calculateOrderTotal(order);

        return this.repository.save({
            ...order,
            total
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The calculation is pure.

The repository handles the side effect.


28. Practical Rules

When writing JavaScript applications, these rules are useful.

Rule 1: Prefer small functions

Instead of:

processEverything();
Enter fullscreen mode Exit fullscreen mode

prefer:

validateOrder();
calculateTotal();
applyDiscount();
saveOrder();
sendNotification();
Enter fullscreen mode Exit fullscreen mode

Rule 2: Minimize shared mutable state

Shared mutable state creates hidden dependencies.

Prefer:

const newState = updateState(oldState);
Enter fullscreen mode Exit fullscreen mode

over uncontrolled global mutation.


Rule 3: Prefer composition over deep inheritance

Instead of:

A
└── B
    └── C
        └── D
Enter fullscreen mode Exit fullscreen mode

consider:

A + capability1 + capability2 + capability3
Enter fullscreen mode Exit fullscreen mode

Rule 4: Keep side effects at the boundaries

Pure:

const calculateTax = (price, rate) =>
    price * rate;
Enter fullscreen mode Exit fullscreen mode

Side effect:

await database.save(order);
Enter fullscreen mode Exit fullscreen mode

Don't mix them unnecessarily.


Rule 5: Use encapsulation where state has invariants

For example:

class BankAccount {
    #balance;

    withdraw(amount) {
        if (amount > this.#balance) {
            throw new Error("Insufficient funds");
        }

        this.#balance -= amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

The object protects its invariant.


29. Final Mental Model

The easiest way to remember the concepts is:

FUNCTIONAL PROGRAMMING
        │
        ├── Functions
        ├── Pure Functions
        ├── Immutability
        ├── Composition
        ├── Currying
        ├── Higher-Order Functions
        └── Recursion


OBJECT-ORIENTED PROGRAMMING
        │
        ├── Objects
        ├── Encapsulation
        ├── Abstraction
        ├── Inheritance
        ├── Polymorphism
        └── Composition


JAVASCRIPT
        │
        ├── Prototype-based
        ├── First-class Functions
        ├── Closures
        ├── Classes
        ├── Functional Features
        └── Object-oriented Features
Enter fullscreen mode Exit fullscreen mode

The most important lesson is that JavaScript does not force you to choose one paradigm.

You can combine them.

For example:

class OrderService {
    constructor(repository) {
        this.repository = repository;
    }

    async process(order) {
        const validated = validateOrder(order);

        const total = calculateTotal(validated);

        const finalOrder = {
            ...validated,
            total
        };

        return this.repository.save(finalOrder);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • OrderService uses OOP
  • validateOrder can be a pure function
  • calculateTotal can be functional
  • { ...validated } uses immutable-style updates
  • repository is encapsulated through the service
  • save() represents a side effect

This hybrid style is often more useful in real-world applications than trying to build an entire system using only FP or only OOP.


Conclusion

Functional Programming and Object-Oriented Programming are not simply collections of syntax features. They are different ways of reasoning about software.

FP focuses on:

Transforming data through predictable functions.

OOP focuses on:

Encapsulating state and behavior inside objects.

JavaScript gives developers the ability to use both.

Understanding prototypes explains how JavaScript objects actually work. Understanding composition helps avoid unnecessarily complex inheritance hierarchies. Higher-order functions, currying, and closures provide powerful functional abstractions. Pure functions and immutability make business logic easier to test and reason about. Private properties and methods provide strong encapsulation when objects need to protect their internal state.

The real goal is not to become "an FP developer" or "an OOP developer."

The goal is to become a developer who can choose the right abstraction for the problem.

Use objects when ownership and state matter.

Use functions when transformation and predictability matter.

Use composition when you need flexibility.

Use immutability when predictable state transitions matter.

And combine the paradigms when the system benefits from both.

Top comments (0)