Skip to main content

Command Palette

Search for a command to run...

Object Oriented Programming in JavaScript: In-Depth

Updated
•7 min read•View as Markdown

Before diving into OOPs concepts, let's understand how an object looks in JavaScript.

const user1 = {
    // Properties
    name: 'Sam',
    age: 22,    

    // Methods
    work: function (company) {
        console.log(`\({this.name} works in \){company}`);
    },

    // newer syntax for methods
    play() {
        console.log(`${this.name} is playing`);
    }
    
}

console.log(user1.name);
user1.work('Noogle');

user1.salary = 100000;
console.log(user1.salary);

In JavaScript, objects have just a bunch of fields/variables with some data as the value or a function. The variables containing data are called properties and the ones having a function are called methods.

In the above example, the variables with their values are written directly during object creation which is called an object literal. The issue with object literals is that there's no structure, it doesn't follow any pattern, and the fields are just decided while object creation. Ex: user1 in our example might look like an user, but it doesn't represent any users, cause there can be an user2 with completely different fields.

So, to give objects a structure constructor functions were introduced.

Constructor Functions

Constructor functions are just normal functions written in a special way to provide a blueprint or structure to our object. Constructor function names are written in PascalCase to distinguish it from normal functions and they don't have a return type.

Inside constructor function we leverage the 'this' keyword to refer to our object and set the fields as per structure and the constructor function needs to be called in a special manner such that 'this' would refer to our object instead of the globalThis.

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

// Correct ways to use the contructor function
let user1 = {};
User.call(user1, 'Bruce', 45);

let user2 = {};
User.apply(user2, ['Clark', 30]);

let user3 = {};
let User3Creator = User.bind(user3);
User3Creator('Diana', 28);

// In all the above example this inside the function would refer to the binded object instead of globalThis.


// Wrong way
User('Peter', 50)    // it simply calls the function, with the 'this' refering to globalThis

Now, the objects created would have some structure as they follow the blueprint of the User, so we can identify them as a user.

NOTE: As we have a blueprint now, if there's some property we'd like think of from the perspective of the blueprint instead of indivial objects, we can store that in the blueprint itself i.e. in out case User (This works because in JavaScript, functions itself are objects). We can directy add more fields to the User and refer them using the dot notation. These fields are called static variables.

User.SPECIES = "homosapiens";     // static variable of User constructor function.

Methods and Prototype Linking

If we want to add methods for our objects, we might be tempted to write it inside the constructor function itself. Which will work, but there's a huge catch which makes it extremely inefficient.

// Inefficient way to add methods

function User(name, age) {
    this.name = name;
    this.age = age;
    
    this.play = function () {
        console.log(`${this.name} is playing`);
    }
}

let user1 = {};
User.call(user1, 'Bruce', 45);

let user2 = {};
User.call(user2, 'Clark', 30);

user1.play();
user2.play();

In the above example, the play() method will work perfectly fine, but eventhough the method definition stays exactly the same, it'll take twice the memory. WHY? cause in JavaScript functions are treated as firstclass citizen, and thus the methods are individually created and take separate memory in each object.

So, to share memory in case the variable holds a function inside an object i.e. a method. We leverage prototype linking.

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

// Memory efficient way to add methods
User.prototype.play = function () {
    console.log(`${this.name} is playing`);
}

let user1 = Object.create(User.prototype);
User.call(user1, 'Bruce', 45);

let user2 = Object.create(User.prototype);
User.call(user2, 'Clark', 30);

user1.play();
user2.play();

Prototype and Prototype Linking

All functions have a property called prototype in which we actually set the methods. For this prototype to reflect in our objects, Javascript uses mechanisms to add secret __proto__ in our object and links it to the prototype of the constructor function. Let's understand it with multiple examples

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

User.prototype.play = function () {
    console.log(`${this.name} is playing`);
}

// Purely manual way
let user1 = { userPrototype: User.prototype }
User.call(user1, 'Bruce', 45);

user1.userPrototype.play();    // correct method is referenced but will print "undefined is playing" cause the caller is User instead of user1.

// Semi-manual way, adhering to JavaScript guidelines
let user2 = { __proto__: User.prototype }
User.call(user2, 'Clark', 30);

user2.__proto__.play();    // "undefined is playing"
user2.play();                  // "Clark is playing"
// The above example works cause JavaScript understands and looks for the __proto__ field, and whose prototype field is attached here, BUT while calling the methods in prototype, it calls from the context of user1 instead of User.

// Automatic ways to achieve it -- Let JavaScript bind the prototype

let user3 = Object.create(User.prototype);

let user4 = {};
Object.setPrototypeOf(user4, User.prototype);

console.log(user3.__proto__ === User.prototype);
console.log(user4.__proto__ === User.prototype);

// now user3 and user4 will have a hidden __proto__ field binded to User.prototype and calling the play method directory from the user3, user4 objects will work

User.call(user3, 'Diana', 1000);
user3.play();

User.call(user4, 'Barry', 23);
user4.play();

NEW keyword

From the above learning, if I wanted to create a helper function to automate the creation of objects from a bluprint. I'd need to follow 4 steps inside the helper function

  1. Create an object.

  2. Link the __proto__ field of the object to the prototype field of the constuctor function.

  3. Bind 'this' of the object to the 'this' used in constuctor function

  4. Return the object

All of these 4 steps can be handled by using a single keyword called new

// Constructor Function i.e Blueprint

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

User.SPECIES = "homosapiens";

User.prototype.play = function () {
    console.log(`${this.name} is playing`);
}

// helper function
function createObject(Constructor, ...args) {
    // 1. create an object
    const obj = {};

    // 2. link the prototype
    Object.setPrototypeOf(obj, Constructor.prototype);

    // 3. bind this of the object
    Constructor.apply(obj, args);

    // 4. return the object
    return obj;
}

// creating object using helper function
const user1 = createObject(User, 'Bruce', 45);

// creating object using new keyword
const user2 = new User('Clark', 30);

// PS: wrong way
const user3 = User('Diana', 1000);    // this is just calling the function and getting undefined cause there's no return type

new is extremely helpful as it completely automates our object creation process taking care of everything. But that's not all new also add more helpful features which we miss out when creating objects manually.

More advantages of new

  • instanceof keyword: objects created with new can be used with instanceof to check the type (Ex: console.log(user1 instanceof User); )

  • constructor property: objects created witn new add another property called constructor to refer to the constructor function from within. (Ex. console.log(user1.constructor.name))

CLASS syntax for writing blueprint

Currently there's a good logical grouping of the object creation process using the new keyword, but there's no proper grouping of the blueprint. This solved by the new class keyword

// Old Style
function User(name, age) {
    this.name = name;
    this.age = age;
    this.friends = [];
}

User.SPECIES = "homosapiens";

User.prototype.play = function () {
    console.log(`${this.name} is playing`);
};

User.prototype.addFriend = function (name) {
    this.friends.push(name);
};

// New Style
class User {
    static SPECIES = "homosapiens";

    friends = [];

    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    play() {
        console.log(`${this.name} is playing`);
    }

    addFriend(name) {
        this.friends.push(name);
    };
}

JavaScript Fundamentals for Developers

Part 9 of 22

Learn JavaScript from the ground up with clear explanations and practical examples. This series covers core JavaScript concepts like variables, arrays, functions, loops, objects, and modern ES6 features to help you build a strong foundation in JavaScript.

Up next

Callback Functions in JS: Why do they Exist?

What is a Callback Function? A callback function is a function that is passed into another function as an argument, which is then invoked inside the outer function to complete a routine or action. To