JavaScript - Object Oriented Programming

Intro

Object-Oriented Programming is a programming paradigm based on the concept of objects, which combine data (properties) and behavior (methods). OOP focuses on modeling real world entities as objects and promotes modularity, reusability, and maintainability.

Disclaimer: Most of the javascript content for my writings will come from the book - Speaking JavaScript. This is fantastic book!

If you are a C++ or Java programmer then you are already aware of Object Oriented Programming and how we utilize it for writing scalable and maintainable code. With the rise of web applications we are writing a lot of JS and with time this will keep growing. In this article we will explore how we achieve OOP in JavaScript using the language constructs while JavaScript is not a object oriented programming language at all!

Classes & Objects

In JS we use the constructor functions to define structure of a class and then to create the instance of that class we use the new operator. Now constructor functions are not any special functions they are the usual functions with some special restrictions.

1
2
3
4
5
6
7
8
9
// constructor function Person
function Person (name, age) {
this.name = name;
this.age = age;
}

// create persons
var john = new Person("John", 20);
var marry = new Person("Marry", 22);

Note: If we miss the new* keyword, this will not create any object. When we use new, it creates the this for the Person object and implicitly returns this keyword.

Methods

We can add behavior of the objects as well by adding methods to the class definition along with properties (like name & age).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// constructor function Person
function Person (name, age) {
this.name = name;
this.age = age;

this.greet = function () {
return "My name is " + this.name;
}

this.walk = function () {
return this.name + " is now walking";
}
}

// create persons
var john = new Person("John", 20);
var marry = new Person("Marry", 22);

// calling methods
console.log(john.greet());
console.log(marry.walk());

Now we have behaviors added to our objects, but there’s an issue when adding methods directly inside function constructors in JavaScript. Every object created using the new operator gets its own copy of each method, which leads to unnecessary memory usage. To avoid this, we place the methods on the constructor’s prototype instead. This way, all objects share the same method through the prototype chain, rather than storing separate copies.

Prototype

This prevents each object from storing its own copy of the method, reducing memory usage. Instead, every object accesses the method via the prototype chain. So instead of attaching methods inside the constructor, we attach them to the prototype like this:

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name, age) {
this.name = name;
this.age = age;
}

Person.prototype.greet = function () {
return "My name is " + this.name;
}

Person.prototype.walk = function () {
return this.name + " is now walking";
}

We can validate that both the objects are referring to the same method instead of making copies via:

1
2
3
4
var john = new Person("John", 20);
var marry = new Person("Marry", 22);

console.log(john.walk === marry.walk); // should be true. means shared not copied

The new Operator

So we can now say that the new operator:

  • Creates an empty object.
  • Set the keyword this to the currently created object.
  • Link the prototype chain to the object created.
  • Returns this implicitly.

Object.create()

In JavaScript, all objects ultimately inherit from Object.prototype, which sits at the top of the prototype chain. The built-in Object constructor provides several commonly used methods such as toString(), create(), freeze(), and so on.

We can use Object.create() passing it an instance of some other class and it will return an empty object of that class having the prototype linked to it.

Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person (name, age) {
this.name = name;
this.age = age;
}

Person.prototype.greet = function () {
return "My name is " + this.name;
}

Person.prototype.walk = function () {
return this.name + " is now walking";
}

var marry = new Person("Marry", 20);

var jemmy = Object.create(marry);
var proto = Object.getPrototypeOf(jemmy);

console.log(jemmy);
console.log("prototype of jemmy =", proto);

In the code above, jemmy is created using Object.create(marry). This creates a new empty object whose prototype is set to the object marry. As a result, jemmy does not have its own properties, but it can access name, age, and the methods defined on Person.prototype through the prototype chain.

When we inspect the prototype of jemmy, we see that it points directly to marry, not to Person.prototype.

1
2
Person {}
prototype of jemmy = Person { name: 'Marry', age: 20 }

If we want jemmy to directly use Person's prototype, we can use something like:

1
2
3
4
5
// create jemmy with Person.prototype as its prototype
var jemmy = Object.create(Person.prototype);

// validate that jemmy is person prototype
console.log(Object.getPrototypeOf(jemmy) === Person.prototype);

Prototypal Inheritance

JavaScript used a prototype based inheritance model rather than classical class based inheritance. Objects inherited directly from other objects through the prototype chain. The example above of Object.create is an example of inheritance in JS. Also one thing to note is that we actually try to simulate inheritance using JS language constructs.

Function Constructor

Let’s see how we can achieve inheritance using constructor functions and prototypes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age) {
this.name = name;
this.age = age
}

Person.prototype.greet = function () {
console.log("Hello, I'm " + this.name);
};

function Student(name, grade) {
// inherit properties
Person.call(this, name);
this.grade = grade;
}

// linking the prototype
Student.prototype = Object.create(Person.prototype);
// important
Student.prototype.constructor = Student;

Calling the parent constructor ensured that instance specific properties (like name & age) were copied onto the child object. This is how method lookup worked internally:

1
student -> Student.prototype -> Person.prototype -> Object.prototype -> null

Reset Constructor

Why we have to reset the Student constructor: Student.prototype.constructor = Student;?

When we use Object.create() to set up inheritance between constructor functions, we overwrite the child’s prototype object. Due to this, the default constructor property is lost and ends up pointing to the parent constructor instead of the child. Without the reset, Student objects appear to be Person objects.

Polymorphism

Since we discussed Inheritance, let’s also cover polymorphism in JS. The definition of polymorphism states that it is the ability of one thing to take multiple forms. This means a single name (method, function, operator) can be used in different ways or represent different behaviors.

We saw the greet() method above that is coming from Person to Student. But let’s tweak the greet method to work differently for Students. We can do this by method overriding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function Person(name, age) {
this.name = name;
this.age = age
}

Person.prototype.greet = function () {
console.log("Hello, I'm " + this.name);
};

function Student(name, grade) {
// inherit properties
Person.call(this, name);
this.grade = grade;
}

// linking the prototype
Student.prototype = Object.create(Person.prototype);
// important
Student.prototype.constructor = Student;

// method overriding
Student.prototype.greet = function () {
console.log("Hello, I'm " + this.name + " and I am a student");
};

var person = new Person("Jerry", 20);
var student = new Student("James", 20);

person.greet();
student.greet();

On running the code we can see that the methods are same but working differently for different objects.

1
2
Hello, I'm Jerry
Hello, I'm James and I am a student

Encapsulation

Encapsulation is the practice of bundling data (properties or variables) and the methods that operate on that data into a single unit called class while restricting direct access to some of the object’s internal details.

For example A BankAccount class hides the balance and allows access only through methods like credit() or debit(). We have already seen that we can bundle method and properties using constructor functions and prototype but let’s see how we enable data or method hiding.

Here’s the code demonstration of using a private variable balance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var BankAccount = (function () {
function BankAccount() {
var balance = 0;

this.getBalance = function () {
return balance;
}

this.setBalance = function (val) {
balance = val;
}
}
BankAccount.prototype.debit = function (amount) {
var balance = this.getBalance();
balance -= amount;
this.setBalance(balance);
};
BankAccount.prototype.credit = function (amount) {
var balance = this.getBalance();
balance += amount;
this.setBalance(balance);
};

BankAccount.prototype.balance = function () {
return this.getBalance()
};

return BankAccount;
}());

const bank = new BankAccount();
bank.credit(100);
console.log(bank.balance());

IIFE

IIFE is called Immediately Invoked Function Expression which is a function that runs as soon as it is defined. In the example above, the first line BankAccount is an IIFE.

Closures

Closure is when a function remembers variables from its outer scope, even after the outer function has finished executing.

balance is defined inside the constructor, the getBalance and setBalance are inner functions that close over balance Even after the constructor finishes, these functions still remember balance for that instance. This allows true privacy, because nothing outside can access balance directly.

1
console.log(bank.balance); // undefined

Abstraction

Abstraction is the process of exposing only the essential features of an object while hiding the complex implementation details. In short, focus on what an object does, not how it does it. In java, abstraction is achieved using interfaces or abstract classes.

For example a Vehicle interface defines a start() method without specifying how different vehicles implement it. let’s look at the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Vehicle(type) {
this.type = type;

// Private method via closure (don't skip var)
var startEngine = function() {
console.log(type + " engine started");
};

this.drive = function() {
startEngine(); // using private method
console.log(type + " is driving");
};
}

var car = new Vehicle("Car");
car.drive();
console.log(car.startEngine); // undefined → private

In the code above, the drive() method is abstracted from the user. The user need not to know about how the car is driving.

Outro

So we saw how we use the language features of JavaScript like functions, closures, prototype and others to simulate the objet oriented programming techniques. it’s a wrap for now.

I hope you enjoyed reading.