Assessing Object-Oriented Support
Both JavaScript and PHP share a common trait: they are interpreted languages without a design-time compilation step. Although modern engines perform runtime compilation for optimization purposes, that detail is irrelevant to this discussion.
For a language to qualify as Object-Oriented, it must exhibit four core characteristics: inheritance, encapsulation, polymorphism, and abstraction.
If these four pillars are present, then we can confidently label JavaScript and PHP as Object-Oriented. Let us examine each one.
Inheritance
Inheritance allows a new class to build upon an existing class [1]. The extends keyword facilitates this mechanism in both languages. The following snippets illustrate the syntax.
// JavaScript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
// PHP
class Animal {
public $name;
public function __construct($name) {
$this->name = $name;
}
public function speak() {
echo "{$this->name} makes a noise.";
}
}
class Dog extends Animal {
public function speak() {
echo "{$this->name} barks.";
}
}
Critics might point out that JavaScript relies on prototypal inheritance rather than the class-based model [2]. Nevertheless, class-based inheritance is merely one approach. Prototypal and class-based strategies are equally legitimate.
Interestingly, the Go language lacks inheritance entirely, yet it is still regarded (somewhat) as Object-Oriented [3]. Given the popularity of "composition over inheritance" in contemporary development, some authorities question whether inheritance is a mandatory OOP feature at all [4].
Regardless, it is unequivocal that both JavaScript and PHP support inheritance.
Encapsulation
Encapsulation shields external code from an object's internal workings [5]. In essence, it hides properties and methods from the outside. JavaScript offers private and public keywords, while PHP adds protected to the mix. These are referred to as access modifiers (or "member visibility" in some circles).
// JavaScript
class Animal {
#privateProperty = 'private';
publicProperty = 'public';
#privateMethod() {
/../
}
publicMethod() {
/../
}
}
// PHP
class Animal {
private $privateProperty = 'private';
protected $protectedProperty = 'protected';
public $publicProperty = 'public';
private function privateMethod() { /../ }
protected function protectedMethod() { /../ }
public function publicMethod() { /../ }
}
One might object that JavaScript lacks a protected keyword. However, access modifiers are not a prerequisite for encapsulation—they are merely implementation specifics. Encapsulation only requires a way to conceal an object's internals, a capability both languages possess.
Thus, it is clear that both JavaScript and PHP offer encapsulation.
NOTE: Before the introduction of class syntax, JavaScript developers used constructor functions to achieve encapsulation [6], which was perfectly acceptable. Even Python, universally acknowledged as Object-Oriented, has no access modifiers—encapsulation is achieved by convention using an underscore prefix.
Polymorphism
Polymorphism lets a single symbol represent multiple types [7]. This broad definition encompasses several specialized forms, including ad-hoc [8], parametric [9], and subtype polymorphism [10].
A detailed exploration of each type is beyond our scope here. For those interested, the cited references offer a solid starting point for further reading [7][8][9][10].
Unsurprisingly, both JavaScript and PHP support polymorphism due to their dynamic typing. In fact, all dynamically typed languages are inherently polymorphic. You can verify this claim by reviewing the referenced materials.
Abstraction
My university curriculum taught that Object-Oriented Programming comprised inheritance, encapsulation, and polymorphism—the "Holy threesome of OOP." Abstraction never made the list.
Simplified, abstraction lets you hide complex implementation details and expose only necessary features [11]. Logically, abstraction in OOP emerges from the combined effect of encapsulation, polymorphism, and inheritance—perhaps explaining why we had a "threesome" rather than a "foursome."
In practice, abstraction is often realized through interfaces and abstract classes.
PHP supports both interfaces and abstract classes without question.
// PHP
interface Animal {
public function speak();
}
class Dog implements Animal {
public function speak() {
echo 'Dog barks.';
}
}
// PHP
abstract class Animal {
public abstract function speak();
}
class Dog extends Animal {
public function speak() {
echo 'Dog barks.';
}
}
Abstraction in conventional class-based OOP relies on subtype polymorphism [10]. This relationship allows code to depend on general abstractions (contracts) instead of concrete implementations, forming the basis of the "Dependency Inversion Principle" [12] from SOLID principles [13]—"Depend upon abstractions, not concrete implementations."
// PHP
class Shelter {
private $animals = [];
public function addAnimal(Animal $animal) {
$this->animals[] = $animal;
}
}
$shelter = new Shelter();
$dog = new Dog();
$shelter->addAnimal($dog);
JavaScript, on the other hand, lacks native interfaces and abstract classes. One might argue it fails to support abstraction, but that is not entirely accurate—it is achievable through userland techniques.
The naive method is "duck typing" (If it walks like a duck, and it quacks like a duck, then it must be a duck) [14]. This approach checks an object for certain properties or methods, treating it as an "instance" of a virtual interface or abstract class. Unfortunately, this is error-prone and inadvisable.
// JavaScript
class Parrot {
fly() {
console.log('Fly my bird, fly!.');
}
}
class Airplane {
fly() {
console.log('Engine started!');
}
}
for (const something of [new Parrot(), new Airplane()]) {
if (typeof something.fly === 'function') {
console.log('It is a bird!'); // Not true for airplane
}
}
JavaScript provides the instanceof operator, which verifies whether a given class constructor appears in an object's prototype chain [15]. By simulating interfaces and abstract classes, we can employ this operator for our purposes.
// JavaScript
/**
* @interface
*/
class Animal {
constructor() {
throw new Error('Interface "Animal" cannot be instantiated.');
}
speak() {
throw new Error('Method of interface "Animal" must be implemented.');
}
}
/**
* @abstract
*/
class Car {
constructor(hasGasoline) {
this.#hasGasoline = hasGasoline;
}
start() {
if (!this.#hasGasoline) {
throw new Error('Car has no gasoline.');
}
console.log('Engine started.');
}
break() {
throw new Error('Method of interface "Animal" must be implemented.');
}
}
class Parrot extends Animal {
constructor() {
super.constructor();
}
speak() {
console.log('Parrot speaks.');
}
}
class Lambo extends Car {
break() {
console.log('The Father has stopped the car.');
}
}
for (const something of [new Parrot(), new Lambo()]) {
if (something instanceof Animal) {
console.log('It is animal!');
}
}
If the "Holy foursome" is truly required for OOP, JavaScript can still satisfy the abstraction criterion through subtype polymorphism—albeit not at the language level.
Final Assessment
We have defined a methodical framework for determining whether a language adheres to the Object-Oriented paradigm. By checking each language against each criterion, it becomes evident that both JavaScript and PHP qualify as Object-Oriented languages without controversy. Every requirement is satisfied.
The final criterion may appear debatable, as JavaScript lacks native interface and abstract class support. However, the assertion "JavaScript prevents us from having interfaces and abstract classes" is demonstrably false. As the code in this article demonstrates, mimicking them is possible, and what is possible is supported.
The crucial takeaway is the establishment of a systematic evaluation framework—a mental model for classifying languages within paradigms. This rigor is essential because misconceptions abound regarding functional programming in JavaScript and PHP.
NOTE: Alan Kay [16], the originator of the Object-Oriented paradigm and the Smalltalk language, would remind us of a missed element: the essence of OOP lies in state encapsulation and message passing [17].
Assessing Functional Programming Support
Object-Oriented design centers on objects, their relationships, and how they communicate. Functional programming, in contrast, revolves around functions and how they combine. The theoretical roots of this paradigm trace back to Alonzo Church's Lambda calculus in the 1930s[18], though LISP, emerging around 1950, is often credited as the first practical functional language.
Much like its Object-Oriented counterpart, the Functional paradigm has a defined checklist of language features. These include first-class and higher-order functions, referential transparency with immutability, pure functions, functional data structures, lazy evaluation, and recursion. Some literature also lists a type system as a requirement; this article will examine that criterion as well, even though a type system primarily serves to prevent invalid programs from failing at runtime.
First-Class and Higher-Order Functions
A language with first-class functions treats them like any other value — they can be stored in variables, passed as parameters, or returned from other functions[19]. Higher-order functions are a specific application of this, capable of accepting functions as input or producing them as output[20].
Both JavaScript and PHP fully support these concepts.
// JavaScript
const add = (a, b) => a + b;
const operator = (a, b, operation) => operation(a, b);
console.log(operator(1, 2, add));
// PHP
$add = static fn ($a, $b) => $a + $b;
$operator = static fn ($a, $b, $operation) => $operation($a, $b);
echo $operator(1, 2, $add);
While the provided examples don't show a function returning another function, both languages are fully capable of that as well. It is clear that JavaScript and PHP meet this fundamental requirement of the Functional paradigm, which is essential for enabling function composition.
Referential Transparency and Immutability
In essence, referential transparency and immutability mean that once a piece of memory holds a value, that value cannot be altered; it can only be released. This constraint is vital because it eliminates side effects, leading to several beneficial outcomes:
- Absence of side effects - Immutable memory means no side effects occur. While there is always a current state of memory, that state cannot be mutated, giving rise to the Functional paradigm's mantra: "no state, no side effects." This predictability makes software simpler to understand, test, and debug.
- Inherent thread safety - Without side effects, the need for locks, semaphores, or other synchronization primitives vanishes. Multiple threads can safely read the same memory location concurrently.
It's important to dispel some common misconceptions about what immutability permits. The following operations, often seen in "functional" JavaScript or PHP code, actually violate the paradigm's core rules:
// JavaScript
function foo(a) {
let b = a++; // Not a functional programming
}
Statements like a++ or a-- are forbidden in a functional context because they directly modify memory.
// PHP
for ($i = 1; i++; $i < 10) {
// Not a functional programming
}
Similarly, standard loop constructs are invalid since they inherently rely on a mutable counter or index.
JavaScript provides the const keyword, which declares a read-only variable for primitives like numbers and strings. However, const only prevents reassignment of the variable itself, not the mutation of the object or array it points to. PHP lacks an equivalent language construct.
This lack of native support is not a significant obstacle, however. The immutability required by the Functional paradigm can be enforced in userland code. This can be achieved through disciplined coding practices, the use of linters and static analysis tools, or a combination of both. If we can avoid mutating allocated memory, we can legitimately claim the language supports referential transparency.
Pure Functions
A pure function is defined by two characteristics: it has no side effects, and its return value is determined solely by its input arguments. Given the requirements of referential transparency and immutability, pure functions are an inevitable outcome. They come with significant advantages:
- Memoization - Because a pure function always returns the same result for the same inputs, its output can be cached indefinitely[21].
- Thread safety - This is a direct consequence of immutability and the absence of side effects.
- Simplified testing - With no side effects, there are no hidden states or external dependencies to mock, making tests straightforward.
- Execution flexibility - If two functions do not depend on each other's data, their execution order can be swapped, or they can even be run in parallel.
Neither PHP nor JavaScript has built-in mechanisms to enforce purity. This does not prevent developers from writing pure functions, making these languages capable of supporting this feature at the application level.
It is worth noting that a function which reads (but does not mutate) variables from an outer scope is still considered pure, provided it has no effects and its output is deterministic based on its inputs.
// JavaScript
function foo(bar) {
return function (baz) {
return bar + baz;
}
}
// PHP
function foo($bar) {
return static fn ($baz) => $bar + $baz;
}
The functions in these examples are pure. Even though they return closures that reference outer-scope variables, they do not produce side effects, and their results depend only on their arguments. They are thread-safe, cacheable, reorderable, and easy to test.
Type System
While I argue that a type system is not a strict prerequisite for the Functional paradigm itself, it is an invaluable asset for constructing reliable applications. PHP offers an optional type system, though it has limitations like a lack of generics. JavaScript does not include one natively. For projects requiring static type checking, this can be implemented in userland using tools like Psalm[22] or PHPStan[23] in the PHP environment.
Functional Data Structures
Functional data structures are, by definition, immutable. This would require JavaScript and PHP to offer native immutable arrays and objects, which they do not. This requirement typically exists because languages designed for functional programming are highly optimized for the performance overhead that immutability entails.
As with other features, this is not a hard barrier. Developers are free to design and use their own immutable structures. The support is therefore available in userland, even if not at the language level.
Lazy Evaluation
Lazy evaluation is a performance optimization technique born from the fact that functional languages can be slower than their imperative counterparts. It works by deferring the evaluation of an expression until its result is actually required.
Consider this example:
// JavaScript
let arr = [1, 2 / 0, 3];
console.log(arr.length);
// PHP
$arr = [1, 2/0, 3];
echo count($arr);
In both JavaScript and PHP, this code will throw a runtime error. However, in a language with lazy evaluation, the expression 2/0 would only be evaluated if it is actually accessed (like when trying to read arr[1]).
While neither PHP nor JavaScript supports lazy evaluation natively, and it cannot be replicated in userland, its absence is not a blocker for using the Functional paradigm. Its primary impact is on execution speed and the timing of certain runtime errors, making it a performance concern rather than a correctness issue.
Recursion
Recursion, where a function calls itself, is the functional alternative to loops. Since the paradigm forbids mutable state, loops are generally unusable, making recursion necessary. While both PHP and JavaScript support recursion, they share a critical flaw: they are prone to stack overflow errors in deep recursion scenarios.
An example best illustrates this issue. Suppose we want to calculate the sum of all integers from 1 to n. For n = 5, this would be 1 + 2 + 3 + 4 + 5 = 15. The imperative approach would use a loop:
// JavaScript
function sum(n) {
let result = 0;
for (let i = 1; i <= n; i++) {
result += i;
}
return result;
}
// PHP
function sum($n) {
$result = 0;
for ($i = 1; $i <= $n; $i++) {
$result += $i;
}
return $result;
}
These loop examples violate the Functional paradigm because they mutate state. Let's look at a recursive alternative:
// JavaScript
function sum(n, carry = 0) {
if (n === 1) {
return 1 + carry;
}
return sum(n - 1, n + carry);
}
// PHP
function sum($n, $carry = 0) {
if ($n === 1) {
return 1 + $carry;
}
return sum($n - 1, $n + $carry);
}
These are pure, non-mutating functions. However, if we invoke sum(100000) in either language, we will encounter a stack overflow. The root cause is that neither language implements tail call optimization[24]. This compiler technique detects when a function call is the final operation, discards the current stack frame, and reuses it for the next call. In a language with TCO, sum(100000) would execute without issue, as it would only ever use a single stack frame.
Final Assessment
We have established a set of criteria to judge whether a language supports the Functional paradigm. Neither JavaScript nor PHP natively supports every item on the list. Some, like the type system and lazy evaluation, are debatable or have minor impact. Others, like immutability, can be achieved in userland. The critical failure point is the absence of tail call optimization.
This single missing feature makes it inherently unsafe to build applications in JavaScript or PHP using the Functional paradigm. The reliability of your program becomes dependent on the size of its input. A single deeply recursive call with a sufficiently large argument can crash the entire application.
Such a scenario must be impossible in a true functional programming language.
Because JavaScript and PHP cannot guarantee this safety, they cannot be considered fully compliant with the Functional paradigm. There is a defined limit to the scope of problems that can be safely tackled in these languages using functional techniques.
Closing remarks
The absence of tail call optimization in JavaScript and PHP means that recursion must often be traded for loops. When you make that swap, the functions you write are no longer strictly pure — they rely on mutable state or other imperative mechanisms, which makes them "impure functions"[25].
This raises an interesting question: if we allow impure functions in a functional program, could we equally argue that adding a few pure functions to an otherwise imperative codebase turns it into a "pure imperative" program? That claim would be absurd, yet the logic is the same.
Everything that makes the Functional programming paradigm worthwhile — referential transparency, easier reasoning, parallelization, memoization — comes from the discipline of pure functions, meaning no state and no side effects. When you allow state and side effects to creep into most of your functions, the paradigm loses its value, and calling what you write functional programming is misleading. This happens more often than it should, especially in front-end development. Developers claim to use a Functional programming paradigm, but their code is full of state mutations and side-effectful operations. You might even spot the this keyword inside functions that are supposedly written in a functional style.
In practice, even with a Functional programming paradigm, you will have to interact with state at some point. How you manage that interaction decides whether your code qualifies as functional or imperative. Imagine a plate of spaghetti: delicious, but it has bits of dirt mixed in. That dirt represents the state and side effects in a "functional" program. If the plate is mostly clean spaghetti with a small, well-separated pile of dirt on the side, you can still enjoy your meal without eating any grime.
Understanding the limitations of your tools is essential. Knowing that JavaScript and PHP do not support tail call optimization, you can see why this recursive implementation is risky:
// JavaScript
function sum(n, carry = 0) {
if (n === 1) {
return 1 + carry;
}
return sum(n - 1, n + carry);
}
With that knowledge, you can choose to rewrite it as an impure function that uses a loop instead:
// JavaScript
function sum(n) {
let result = 0;
for (let i = 1; i <= n; i++) {
result += i;
}
return result;
}
From an external perspective, that impure function behaves like a pure one. It is thread-safe, can be run in parallel, its execution can be reordered, it can be memoized, and testing it is straightforward. Whether the function qualifies as part of a Functional programming paradigm comes down to the intent behind its implementation. If you wrote it that way deliberately, because you knew that JavaScript and PHP lack tail call optimization and that large inputs could cause a stack overflow, then even though the function is not strictly pure, it can be treated as such.
If you arrived at that implementation by accident, you were simply writing imperative code.
References
[1] https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)
[2] https://en.wikipedia.org/wiki/Prototype-based_programming
[3] https://go.dev/doc/faq#Is_Go_an_object-oriented_language
[4] https://www.youtube.com/watch?v=xcpSLRpOMJM
[5] https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
[6] https://www.crockford.com/javascript/private.html
[7] https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
[8] https://en.wikipedia.org/wiki/Ad_hoc_polymorphism
[9] https://en.wikipedia.org/wiki/Parametric_polymorphism
[10] https://en.wikipedia.org/wiki/Subtyping
[11] https://en.wikipedia.org/wiki/Object-oriented_programming
[12] https://en.wikipedia.org/wiki/Dependency_inversion_principle
[13] https://en.wikipedia.org/wiki/SOLID
[14] https://en.wikipedia.org/wiki/Duck_typing
[15] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof
[16] https://en.wikipedia.org/wiki/Alan_Kay
[17] https://wiki.c2.com/?AlanKayOnMessaging
[18] https://en.wikipedia.org/wiki/Lambda_calculus
[19] https://en.wikipedia.org/wiki/First-class_function
[20] https://en.wikipedia.org/wiki/Higher-order_function
[21] https://en.wikipedia.org/wiki/Memoization
[22] https://psalm.dev
[23] https://phpstan.org
[24] https://en.wikipedia.org/wiki/Tail_call
[25] https://en.wikipedia.org/wiki/Pure_function

