The Yin and Yang of JavaScript: Exploring OOP and Functional Programming I
JavaScript is a popular language that’s everywhere in the digital world. You can build websites, power servers with Node.js, build desktop apps (Electron.js, Tauri.js) and even make mobile apps with tools like React Native. But what’s so special about JavaScript is its blend of two main ways to program: Object-Oriented Programming (OOP) and Functional Programming.
JavaScript’s OOP is distinctive due to its use of prototypal inheritance, which more closely resembles delegation than classical inheritance. Unlike other languages that rely on classes as blueprints for creating objects, JavaScript employs prototypes. These are existing objects that new objects can delegate to, allowing for a powerful and flexible approach to object creation and organisation, as well as object composition.
On the other hand, Functional Programming in JavaScript enables developers to write cleaner and more maintainable code through concepts like immutability, pure functions, function compositions, and using functions to return objects (factory functions) as well as to compose objects.
These two styles, while different, unite to give JavaScript its flexibility and power. In this article, we will dive deeper into OOP and Functional Programming, and explore how these unique features shape JavaScript into the versatile language it is today. By the end of the article you should have an understanding about the Object Oriented Programming concepts in JavaScript, and functional programming paradigms.
Building Blocks of JavaScript: From Data Types to Prototypal Inheritance
Despite a common misconception, JavaScript is not a typeless language. It does, in fact, have data types, including: string
, number
, null
, undefined
, boolean
, bigInt
, Symbol
, object
, and function
. The crucial distinction is that JavaScript is dynamically typed. This means that the JavaScript engine infers the type of a variable based on its current value, and this type can change over time as different values are assigned to the variable. For instance, a single variable can initially hold a string, then a number, and then a boolean, all within the same execution:
let dynamicVar = 'Hello, world!'; // dynamicVar is a string
console.log(typeof dynamicVar); // logs 'string'
dynamicVar = 42; // now dynamicVar is a number
console.log(typeof dynamicVar); // logs 'number'
dynamicVar = true; // now dynamicVar is a boolean
console.log(typeof dynamicVar); // logs 'boolean'
Furthermore, JavaScript performs automatic type conversions in certain situations, a feature known as type coercion. This can lead to unexpected results, as in the following example:
let num = '5' + 2;
console.log(num); // logs '52', not 7
Here, JavaScript coerces the number 2
into a string in order to perform the +
operation, resulting in string concatenation rather than numeric addition. Thus, while JavaScript's dynamic typing provides flexibility, it also requires a solid understanding of these behaviours to avoid potential pitfalls.
In the following table we have the primitive data types represented in JavaScript.
In JavaScript, we have both primitive and non-primitive data types. The primitive data types include undefined
, string
, number
, boolean
, bigint
, symbol
, and surprisingly, null
, despite the famous quirk that typeof null === 'object'
. This behaviour is a longstanding bug in the language which is unlikely to be fixed, as changing the behaviour now would break any existing code that depends on it.
For non-primitive data types, we have object
and function
. It's worth noting that arrays in JavaScript, while they may seem like a distinct data type, are actually a kind of object. For instance, in the example below:
const cities = ["Berlin", "New York", "London"];
typeof cities; // 'object'
Here's a hypothetical illustration of how the engine might implement arrays:
const cities = {
0: "Berlin",
1: "New York",
2: "London"
};
Of course, the actual Array
function constructor provides additional methods and properties that enable arrays to behave in the way we're familiar with.
Now, let’s follow with some interesting behavior of JavaScript, like:
let hexColor = '#FF5733'; // This is a string primitive
console.log(typeof hexColor); // Logs 'string'
// Despite being a primitive, we can use the `toLowerCase` method on it
console.log(hexColor.toLowerCase()); // Logs '#ff5733'
// We can also use the `slice` method to get a part of the string
console.log(hexColor.slice(1, 3)); // Logs 'FF'
// The endsWith method returns a Boolean, matching the end of
//the string with the passed value
console.log(hexColor.endsWith('3')); // Logs true
console.log(hexColor.endsWith('t')); // Logs false
This example raises intriguing questions. Despite hexColor
being a primitive data type, how is it capable of acting like an object with various methods? And where do these methods come from? These questions lie at the heart of JavaScript's dynamic nature and the concept of autoboxing
.
Autoboxing is a powerful feature in JavaScript that enables primitive values to be used as if they were objects. In JavaScript, primitives like strings, numbers, and boolean have corresponding constructor functions: String
, Number
, and Boolean
, respectively. When a method is invoked on a primitive, JavaScript implicitly creates an instance of the corresponding constructor, effectively "wrapping" the primitive in an object, hence the term "autoboxing". This temporary object inherits methods from the prototype of the constructor function. For instance:
let text = "Hello JavaScript!"; // This is a string primitive
console.log(text.toUpperCase()); // HELLO JAVASCRIPT!
// Internally, JavaScript does something similar to this:
let temporaryObject = new String(text);
console.log(temporaryObject.toUpperCase()); // HELLO JAVASCRIPT!
On the other hand, we can compare it with Java where you have to be more explicit and verbose regarding this. In Java, autoboxing is the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes. For example, converting an int
to an Integer
, a double
to a Double
, etc. Here's how this works in Java:
Integer myInteger = 5; // Autoboxing of int
int myInt = myInteger; // Unboxing of Integer
System.out.println(myInteger); // Prints: 5
System.out.println(myInt); // Prints: 5
String myString = "Hello Java!";
String upperCaseString = myString.toUpperCase();
System.out.println(upperCaseString); // Prints: HELLO JAVA!
Now, let’s come back to JavaScript. We showed this example:
let text = "Hello JavaScript!"; // This is a string primitive
console.log(text.toUpperCase()); // HELLO JAVASCRIPT!
// Internally, JavaScript does something similar to this:
let temporaryObject = new String(text);
console.log(temporaryObject.toUpperCase()); // HELLO JAVASCRIPT!
//typeof temporaryObject will be of course object
console.log(typeof temporaryObject); // Logs object
If we type the object in the dev console and expand the String object then what we see is an object with indexed keys that acts like an array but it’s not an array, it also has length which denotes the length of the characters of the string.
But what we don’t see is where all of those methods that are available for strings like indexOf, charAt, endsWith, italics, split, slice, match, toUpperCase, and so on, are actually located.
Turns out if you click this mysterious [[Prototype]]
internal property it opens a new Object where the methods available to a string are available.
let temporaryObject = {
0: "H",
1: "e",
2: "l",
3: "l",
4: "o",
5: " ",
6: "J",
7: "a",
8: "v",
9: "a",
10: "S",
11: "c",
12: "r",
13: "i",
14: "p",
15: "t",
length: 16
// code below is just for illustration, it's not a valid JS syntax but
// is made just to prove the point, the => is not arrow function, just a
// symbol chosen by me to indicate relationship of prototypal inheritance
// __proto__ or [[Prototype]] to denote the prototype.
__proto__: => String.prototype = {
anchor: function() {},
toUpperCase: function() {},
replace: function() {},
// rest of methods
// __proto__ or [[Prototype]] to denote the prototype.
__proto__: => Object.prototype = {
hasOwnProperty: function() {},
isPrototypeOf: function() {},
toString: function(0 {},
// __proto__ or [[Prototype]] to denote the prototype.
__proto__: null
}
}
};
We can confirm the above statement that the proto of temporaryObject
is String.prototype
by comparing it in the browser developer console.
temporaryObject.__proto__ === String.prototype // logs true
Inheritance in JavaScript works differently from traditional implementations found in languages such as Java, C++, and C#. Instead, JavaScript employs a concept known as delegation inheritance. When a property or method is invoked on an object, the JavaScript engine first searches within that object. If it’s unable to locate the property or method there, it traverses up a chain of linked prototypes — starting from the object itself, through its ancestors in the prototype chain, until it reaches the end where Object.prototype.__proto__
is null. This process enables properties and methods to be effectively ‘inherited’ from ancestor objects in the chain.
In JavaScript, the Object.create()
method creates a new object, with the specified object as its prototype. Here, the entire object is set as the prototype, which includes all its properties and methods:
let User = {
type: "User",
displayType: function() {
console.log(this.type);
},
};
let Admin = Object.create(User);
Admin.manageUsers = function() {
console.log("Managing Users");
};
let admin1 = Object.create(Admin);
admin1.displayType(); // logs 'User'
admin1.manageUsers(); // logs 'Managing Users'
However, with constructor functions, the inheritance works differently. When you create an instance using a constructor function, properties defined within the constructor (those attached with this
) are copied to the new object. These properties will exist on each instance created with the constructor.
Methods and properties attached to the constructor’s prototype
are not copied to the instance. Instead, they are made available to the instance through the prototype chain. These properties and methods are shared among all instances of the constructor, instead of being copied to each one. This can be beneficial for memory management, as these shared properties are only stored once in memory.
function User() {
this.type = "User"; // Will be copied to each instance
};
User.prototype.displayType = function() {
// Available through prototype chain
console.log(this.type);
};
function Admin() {
User.call(this); // inherit User properties
this.controls = true; // Will be copied to each instance
};
Admin.prototype = Object.create(User.prototype); // inherit User methods
Admin.prototype.constructor = Admin; // set constructor back to Admin
Admin.prototype.manageUsers = function() {
// Available through prototype chain
console.log("Managing Users");
};
let admin1 = new Admin();
admin1.displayType(); // logs 'User'
admin1.manageUsers(); // logs 'Managing Users'
In this way, JavaScript distinguishes between properties and methods that should be copied to each instance, and those that should be shared among all instances via the prototype chain.
With the introduction of ES6, JavaScript got the class
keyword, which offers a more straightforward syntax to set up inheritance, especially for developers familiar with classical OOP languages like Java, C++, or C#. However, under the hood, JavaScript's classes still operate on the principle of prototypal inheritance - they are merely syntactic sugar over JavaScript's existing prototype-based inheritance. When transpiled, class declarations and class expressions in JavaScript transform into constructor function code.
Here’s how you can implement the previous example using classes:
class User {
constructor() {
this.type = "User";
}
displayType() {
console.log(this.type);
}
}
class Admin extends User {
constructor() {
super();
this.controls = true;
}
manageUsers() {
console.log("Managing Users");
}
}
let admin1 = new Admin();
admin1.displayType(); // logs 'User'
admin1.manageUsers(); // logs 'Managing Users'
In the code above, the extends
keyword is used to create a subclass, Admin
, from the superclass, User
. The super()
function call is used to access and call functions on an object's parent, thus it helps in accessing the parent's constructor and its properties. The methods inside the class are added to the prototype of the object created.
But remember, even if it seems like we are dealing with class-based inheritance because of the new syntax, JavaScript still utilizes prototypal inheritance under the hood. This new syntax simply offers a more familiar structure for programmers coming from a class-based inheritance background.
In JavaScript, methods and properties defined inside the constructor
function in a class work similarly to the ones defined inside the function of a constructor function. They become instance-specific – that is, each instance of the class (or constructor function) gets its own copy of these methods and properties.
In contrast, methods declared inside the class body but outside the constructor
function behave like methods declared in the prototype
property of a constructor function. They are shared among all instances of the class, meaning they exist only in one place (on the class's prototype), and all instances have access to them.
This is a key part of JavaScript’s prototypal inheritance mechanism and a crucial aspect of memory management in JavaScript applications. By defining methods on the prototype rather than on the instance itself, memory usage can be significantly reduced because only one copy of each method needs to be created.
JavaScript’s Twist on OOP: Objects Without Classes
In most object-oriented programming languages, the concept of ‘class’ serves as the blueprint for creating objects. However, JavaScript paints a different picture of object-oriented programming, one that doesn’t rely on classes. While it does include a class
keyword for syntactic sugar, under the hood, it operates on a fundamentally different principle. Instead of classes, JavaScript uses objects as the primary building blocks. This design reduces a level of abstraction, making objects themselves the blueprint for creating more objects. This rare approach opens up a new dimension of flexibility in programming and aligns with JavaScript's dynamic nature. In the following sections, we will delve deeper into this 'classless' structure of JavaScript, exploring how objects come to life and interact with each other.
Below is a diagram showing how classical inheritance works. A class is like a blueprint for creating objects. Objects are instances of classes, created at runtime.
In JavaScript, we bypass the need for the class abstraction. This might sound contradictory, considering we’ve just discussed the class
keyword. But remember, the class
keyword was introduced mainly to align JavaScript more closely with class-based languages, easing the transition for developers accustomed to that paradigm. The fundamental reality remains that in JavaScript, everything revolves around objects.
Objects can be created directly using literal syntax, {}
, which is also known as an 'object literal'. This dynamic, on-the-fly creation of objects is a distinct feature of JavaScript.
Moreover, objects can be linked together using prototypal inheritance. This is commonly done using Object.create()
, which enables an object to directly inherit from another object's prototype. This approach is particularly useful when you want to avoid using the new
keyword.
Consider the following example:
let carPrototype = {
startEngine: function() {
return 'The engine starts...';
}
};
let myCar = Object.assign(Object.create(carPrototype), {
brand: 'Toyota',
color: 'red'
});
console.log(myCar.startEngine()); // "The engine starts..."
console.log(myCar.brand); // "Toyota"
console.log(myCar.color); // "red"
In this example, myCar
directly inherits the properties and methods from carPrototype
, enabling us to invoke startEngine()
, and access the brand
and color
properties directly on myCar
.
One advantage of this object oriented programming in JavaScript is the dynamic runtime it provides.
To continue with the previous code example
// PREVIOUS EXAMPLE
let carPrototype = {
startEngine: function() {
return 'The engine starts...';
}
};
let myCar = Object.assign(Object.create(carPrototype), {
brand: 'Toyota',
color: 'red'
});
console.log(myCar.startEngine()); // "The engine starts..."
console.log(myCar.brand); // "Toyota"
console.log(myCar.color); // "red"
// DYNAMIC RUNTIME IN JS
// Add a new field to carPrototype
carPrototype.fuelType = 'Gasoline';
console.log(carPrototype.fuelType); // "Gasoline"
// Access the new field via myCar
console.log(myCar.fuelType); // "Gasoline"
As you can see, we can add a new property to carPrototype
during runtime, and that property will immediately become available to myCar
. This dynamic behavior allows you to add, modify, or delete properties and methods on the fly, which can lead to more flexible and adaptive code structures. It's especially powerful when you need to adjust an object's behavior based on changing conditions or requirements, without needing to create a new instance or redefine existing structures.
Unraveling JavaScript’s Heart: The Function-Object Interplay and Prototypal Inheritance
In this section we will explore the core feature of JS Object Oriented Programming paradigm, and the prototypal inheritance the Function
and Object
function-objects and their interplay between each other which lies at the heart of JavaScript on how they provide the prototypal inheritance.
Function: In JavaScript, a function is not just a callable object, it’s an entity crafted by the Function()
constructor, which sits at the top of the prototypal inheritance chain, from which all user-defined or custom functions draw their characteristics and abilities like call()
, apply()
, and bind()
. Therefore, every function in JavaScript is effectively an instance of the Function()
constructor.
Every function in JavaScript comes with a .prototype
property. This is not the prototype of the function itself, but the prototype of new instances created by that function when used as a constructor.
function exampleFunction() {};
console.log(exampleFunction.__proto__ === Function.prototype); // true
// Constructor function
function User() {}
// As already said, whatever will be put under the .prototype object will
// be inherited by other functions or objects.
User.prototype.type = 'User';
const user1 = new User();
console.log(user1.type) // logs 'User' from User.prototype.type
const user2 = new User();
console.log(user2.type) // logs 'User' from User.prototype.type
// notice how both user1 and user2 refer to the same property declared once
// in memory: User.prototype.type
Object: Similarly, an object in JavaScript is an entity created by the Object()
constructor, inheriting from Object.prototype
. The Object.prototype
is the base prototype from which all other objects inherit properties and methods. It provides foundational methods like toString()
, hasOwnProperty()
, and isPrototypeOf()
, available to any object.
let exampleObject = {};
console.log(exampleObject.__proto__ === Object.prototype); // true
Knowing that functions in JS can actually have properties the Object function-object have some very useful properties used in JS like Object.create()
, Object.assign()
, Object.entries()
, Object.keys()
, Object.values()
, Object.getPrototypeOf()
etc, etc…
// A demonstration of the statement that functions can have properties
// like objects do
function randomFunction() {}
// Below we assign properties to the function, they are not inherited
// if we create new object via new, they are properties exclusively
// of randomFunction, in a way they act like static in a class.
randomFunction.randomProperty = 'RANDOM_PROPERTY';
randomFunction.randomMethod = function () {
return 'RANDOM_METHOD';
}
Below I will present a diagram which visualizes how Function
and Object
interplay between each other, they have an almost cyclical inter-relationship dependency between each other up to the point where the cycle is broken with Object.prototype.__proto__
being null.
All function instances derive from Function.prototype including the Object function itself, ultimately function instances derive from Object.prototype via Function.prototype itself since the proto of Function.prototype is Object.prototype.
Object instances __proto__/[[Prototype]] is Object.prototype.
On the diagram below, and on the code example of how hypothetically JS Engine would implement this relationship, I emphasised the peculiar relationship on the top of how the prototypical chain is available in JS, it’s through this Function-Object interplay. From the diagram below we can deduce that:
Function’s constructor
__proto__ is actually Function.prototype itself, and the __proto__ ofFunction.prototype
is Object.prototype.Object.prototype
__proto__ is null, and the prototype chain ends here.- But, the __proto__ of
Object constructor
itself is actuallyFunction.prototype
, Object is technically a function data type.
console.log(typeof Object); // "function"
The inter-relationship of the prototype chain between Object and Function and respectively Object.prototype and Function.prototype properties of both constructor functions is somewhat recursive/cyclic but eventually breaks with Object.prototype.__proto__
being null.
// Object constructor
// ==============================================
function Object() { /* ... */ }
// Object.keys()
// Object.observe()
// ...
// `Object.__proto__` (internal [[Prototype]])
// -----------------------------------------------
// Since `Object` is a function, it inherits all of Function's
// instance methods (the ones inside of Function.prototype).
//
// In other words the `Object` constructor can use methods
// like `apply()`, `call()`, `bind()`, and more.
//
// So we can say that the Object's prototype is the
// `Function.prototype` object.
Object.__proto__ = Function.prototype;
// `Object.prototype` (instance methods)
// -----------------------------------------------
// The Object's `prototype` property is totally different from
// the `__proto__` property. This `prototype` property includes
// methods that all JavaScript objects inherit. So an object
// literal like `var obj = {}` or an array like `var arr = []`
// or even a function like `alert` can use these methods.
Object.prototype = {
constructor: Object,
hasOwnProperty: function() {},
isPrototypeOf: function() {},
__proto__: null,
};
// Function constructor
// ==============================================
function Function() { /* ... */ }
// Function.call()
// Function.apply()
// ...
// [[Prototype]] + instance methods
// -----------------------------------------------
// Since `Function` is a function itself and at the same time
// the constructor for other JavaScript functions, its internal
// [[Prototype]] and the `prototype` property point to the same
// exact object.
Function.__proto__ = Function.prototype = function() {
apply: function() {},
call: function() {},
bind: function() {},
//...
// Just an object literal, so it inherits the
// Object's instance methods.
__proto__: Object.prototype
};
// Credit for the answer: https://stackoverflow.com/a/29814731
// I included __proto__ to null for Object.prototype to fully grasp
// the end of cycle and set the Function.prototype to type of function.
// Worth to note that using directly the __proto__ is discouraged
// and the use of it in this example is purely out for illustration.
// The whole example is just for illustration purposes, JavaScript Engine
// will handle everything.
Up Next
In this part, we’ve explored the object-oriented programming (OOP) side of JavaScript. OOP offers power and flexibility and can make our JavaScript code easier to read and organise. But it’s not the only way to code. Some situations call for a different approach. As the saying goes, the right tool for the right job.
In the next part of this series, we’ll turn our attention to functional programming, a contrasting but complementary way of coding. Just as yin and yang represent balance and duality, so does OOP and functional programming in the realm of JavaScript.
In ‘The Yin and Yang of JavaScript: Exploring OOP and Functional Programming II’, we’ll discover the key concepts of functional programming and how it can help us write code that’s not just different, but potentially more efficient and reliable in some scenarios. So, join us in the next article as we continue our journey into the yin and yang of JavaScript.