Skip to main content

Programming paradigms in JavaScript

A paradigm is a way to organize reasoning and state change. One language can support several paradigms, and one program can combine them. A useful choice depends on the problem, constraints and expected evidence—not on a universal ranking.

One problem, three organizations

Problem: from potentially invalid grades, retain numbers from 0 through 20 and compute their average. For [12, "absent", 16, 25], the expected result is 14.

Imperative style

The code describes steps and changes local variables:

function averageValidImperative(values) {
let total = 0;
let count = 0;

for (const value of values) {
if (typeof value === 'number' && value >= 0 && value <= 20) {
total += value;
count += 1;
}
}

if (count === 0) return null;
return total / count;
}

State changes in total and count. This form makes operation order explicit.

Functional style

The code composes transformations and avoids changing the received collection:

const isValidGrade = (value) =>
typeof value === 'number' && value >= 0 && value <= 20;

function averageValidFunctional(values) {
const valid = values.filter(isValidGrade);
if (valid.length === 0) return null;
return valid.reduce((sum, value) => sum + value, 0) / valid.length;
}

filter and reduce declare transformations. They do not automatically make a function pure or fast: effects still need inspection and real context still needs measurement.

Object-oriented style

The code groups grade-book data with operations that maintain its contract:

class GradeBook {
constructor(values) {
this.values = [...values];
}

averageValid() {
const valid = this.values.filter(isValidGrade);
if (valid.length === 0) return null;
return valid.reduce((sum, value) => sum + value, 0) / valid.length;
}
}

This organization becomes useful when a grade book owns several coherent rules and operations. Creating a class for one function does not automatically create a better model.

All three versions must satisfy the same observable contract:

const input = [12, 'absent', 16, 25];
console.assert(averageValidImperative(input) === 14);
console.assert(averageValidFunctional(input) === 14);
console.assert(new GradeBook(input).averageValid() === 14);

Imperative, declarative, functional and object-oriented

  • Imperative describes a sequence of commands and state changes.
  • Declarative expresses a result or constraints while another layer chooses the steps. A SQL query is a common example.
  • Functional favors function composition, immutable values and controlled effects. JavaScript enables this style without enforcing it.
  • Object-oriented organizes data and behavior around objects and their responsibilities. Inheritance is only one possible mechanism; composition is often simpler.

Primitive values and objects in JavaScript

JavaScript has seven primitive types: string, number, bigint, boolean, undefined, symbol and null. A primitive value is not an object and cannot retain an added property. When code calls "dev".toUpperCase(), the language temporarily provides access to methods from the corresponding prototype.

Everything else is an object, including arrays and functions. Two historical details matter: typeof null returns "object" even though null is primitive; typeof returns "function" for a function, while the function remains an object that can own properties.

typeof 'dev'; // "string"
typeof null; // "object" — historical behavior
typeof function () {}; // "function"
Array.isArray([]); // true

Prototypes and classes

Every ordinary object can delegate property lookup to a prototype. That chain still exists when class syntax is used.

const gradeBookMethods = {
size() {
return this.values.length;
},
};

const book = Object.create(gradeBookMethods);
book.values = [12, 16];
console.assert(book.size() === 2);
console.assert(Object.getPrototypeOf(book) === gradeBookMethods);

class provides structured syntax and rules for constructors, methods, private fields and inheritance. It does not replace the prototype model: GradeBook methods live on GradeBook.prototype.

console.assert(
Object.getPrototypeOf(new GradeBook([])) === GradeBook.prototype,
);

Choose from a constraint

First ask: which state changes, who owns it, which contract must remain true and how can it be observed? An imperative loop may be clearest; a functional chain may expose a transformation; an object may protect a shared invariant. None of those choices alone guarantees performance, testability or maintainability.

Observable check

Implement all three average versions, then verify:

const cases = [
{input: [12, 'absent', 16, 25], expected: 14},
{input: [], expected: null},
{input: [-1, 21], expected: null},
];

The check passes when all three versions return the expected result, do not modify input, and you can show where state lives and which mechanism organizes each calculation.