My Logo

PUBLISHED JUNE 29, 2024

Teghen Donald Ticha
Lastly Updated: 3 months ago
Reading time 15 mins

Understanding the this Keyword and Object Prototypes in JavaScript

Explore the intricacies of the "this" keyword and object prototypes in JavaScript. This guide examines their contexts, common pitfalls, and some practical examples.
Understanding the this Keyword and Object Prototypes in JavaScript

Prerequisite

Before jumping in, it is recommended that you have a basic understanding of:

  1. 1.JavaScript Fundamentals: Familiarity with JavaScript syntax, variables, functions, and basic control structures.
  2. 2.Objects in JavaScript: Basic knowledge of creating and manipulating objects.
  3. 3.JavaScript Functions: Understanding of function declarations, expressions, and arrow functions.
  4. 4.ES6 Features: Basic familiarity with ES6 (ECMAScript 2015) features like let/const, template literals, and arrow functions.

Having these foundational skills will help you grasp the concepts of the `this` keyword and `object prototypes` more effectively.

Introduction

In JavaScript, two concepts that often perplex developers(counting myself in 🫣), especially those new to the language, are the `this` keyword and object prototypes.

Understanding these concepts is crucial for writing efficient and effective JavaScript code.

The `this` keyword is used in various contexts and can behave differently depending on where it is used.

`Object prototypes`, on the other hand, form the basis of JavaScript's inheritance system, enabling objects to inherit properties and methods from other objects.


Grasping the intricacies of `this` and `prototypes` will not only help you avoid common pitfalls but also enable you to write more robust and maintainable code.

This comprehensive guide aims to demystify these concepts by providing clear explanations, practical examples, and highlighting common issues and best practices.


By the end of this guide, you will have a solid grasp of how `this` works in different contexts, how to effectively use prototypes for inheritance, and how to apply these concepts in real-world scenarios.

The `this` keyword

In JavaScript, this is a special keyword that refers to the context in which a function is executed.

It is dynamically scoped, meaning its value depends on how the function is called rather than where it is defined.

Understanding this is critical for mastering function execution contexts and object-oriented programming in JavaScript.


Ok wait! If the concept is so confusing why include it in the language right? Well, let's see why.

function bark () {
    console.log(`${this.name} says wooooof`);
}

function fly () {
    console.log(`${this.name} is taking off`);
}

function laugh () {
    console.log(`${this.name} sounds hahaha!`);
}

// define objects
const dog = {
    name : 'Budy'
}
const bird = {
    name : 'Rocky'
}
const person = {
    name : 'Yohan'
}

// 
bark.call(dog); // Output: Budy says wooooof
fly.call(bird); // Output: Rocky is taking off
laugh.call(person); // Output: Yohan sounds hahaha!

As you can see, it removes the need to define a method for each object individually.

So, with the laugh function in place, you can create as many person objects as you like and simply invoke the function and pass the object as context to the `call` method.

It provides a more elegant way of `implicitly passing` along an object reference, leading to cleaner API design and easier reuse.

How `this` is Determined

The value of this is determined by the context in which a function is invoked. Let’s explore the different contexts:

1. Global Context

In the global execution context (outside of any function), this refers to the global object.

The global object is the window object in a browser environment and the global object in a node environment.

console.log(this); // In a browser, this will log the window object but in node it logs {}

function globalContextExample() {
  console.log(this); 
}

globalContextExample(); //  window object in broswer and global object in node

Breakdown:

Top-Level this :

  • In a Node.js module, the top-level this is not the global object. Instead, it refers to the module itself (i.e., an empty object {}) since there is no exported object.
  • This is different from the browser, where the top-level this refers to the window object.


Function Invocation this :

  • When a function is invoked in the global context, this refers to the global object (global in Node.js).
  • In the browser, this inside a function invoked in the global context refers to the window object.

2.Function Context

When a function is invoked as a standalone function, this refers to the global object (in `non-strict mode`) or undefined (in `strict mode`).

function strictFn() {
    'use strict'
  console.log(this); 
}

function nonStrictFn() {
  console.log(this); 
}

strictFn(); // Outputs : undefined
nonStrictFn(); // Outputs : global for node / window for browser


3. Method Context

When a function is called as a method of an object, this refers to the object the method is called on.

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);
    }
  };
  
  person.getName(); // Output: Yohan

  const meth = person.getName; // creates a ref to the person's method
  meth.call({name: 'Mandella'}); // Output: Mandella

Breakdown: getName is called as a method of obj, so this refers to obj, and this.name accesses the name property of obj.


4. Constructor Context

When a function is invoked with the new keyword, this refers to the newly created object.

function Person (name) {    
    this.name = name ;    
    console.log(this) // Output : Person { name: 'Yohan' }
};
  
const person = new Person('Yohan');
console.log(person.name); //  Output: Yohan

Breakdown: When Person is called with new, a new object is created, and this refers to that new object. The name property is assigned to this new object.


5. Arrow Functions

Arrow functions have a lexical this, meaning they do not have their own this context but inherit it from the enclosing non-arrow function or scope.

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);
      (() => {
        console.log('From Arrow: ', this.name)
      })()
    },
    getNameArrow: () => {
        console.log(this.name); //
    }
  };
  
  person.getName(); // Output: Yohan  && From Arrow:  Yohan
  person.getNameArrow(); //  Output:undefined
  
  const meth = person.getName; // creates a ref to the getName method
  const methArrow = person.getNameArrow; // creates a ref to the getNameArrow method

  meth.call({name: 'Mandella'}); // Output: Mandella && From Arrow:  Yohan
  methArrow.call({name: 'Mandella'}); // Output:undefined

Breakdown: getName is a regular function and thus has its own this, which refers to person.
The arrow function getNameArrow does not have its own this and instead inherits this from lexical scope, which is undefined.

However, looking at the IIFE (() => {console.log('From Arrow:', this.name) })(), notice how the outpout corresponds to that of getName. This because it's lexical scope is getName which has a this referring to `person`.


Common Pitfalls with this
Issues with Losing this Context

One of the most common issues in JavaScript is losing the this context.

This often occurs when passing methods as callbacks or when methods are assigned to variables.

Here are some scenarios where this context might be lost:

1. Callback Functions:

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);     
    }    
  };
  
  person.getName(); //  Output: Yohan
setTimeout(person.getName, 10); // Output: undefined

Breakdown: When obj.getName is passed as a callback to setTimeout, it loses its context, and this becomes undefined in strict mode or the global object in non-strict mode.


2. Assigning Methods to Variables:

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);     
    }    
  };
  
person.getName(); //  Output: Yohan
const retrieveName = person.getName;  
retreiveName(); // Output: undefined

Breakdown: Assigning obj.getName to a variable retrieveName loses the original context, and this is no longer obj.



3. Using this Inside Nested Functions

const person = {
    name: 'Yohan',
    getName: function() {
      function retrieveName() {
        console.log(this.name);
      }   
      retrieveName(); 
    }    
  };

person.getName(); // Output : undefined

Breakdown: At invocation, the nested function retrieveName has its this referring to the global / window object (in non strict mode) or undefined (in strict mode).


Solutions to the problem of losing the this context

To address issues with losing this context, JavaScript provides several methods to explicitly bind this: bind(), call(), and apply().

Using bind()

The bind() method creates a new function that, when called, has its this keyword set to the bound value.

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);     
    }    
  };

const retreiveName = person.getName.bind(person); // explicit binding

setTimeout(retreiveName, 10); //  Output: Yohan
retreiveName(); //  Output: Yohan


Using call() and apply()

The call() and apply() methods call a function with a given `this` value and arguments provided individually (call()) or as an array (apply()).

const person = {
    name: 'Yohan',
    getName: function() {
      console.log(this.name);     
    }    
  };

const anotherPerson = {
    name: 'Teghen'
}

setTimeout(() => {
    person.getName.call(person); //  Output: Yohan
    person.getName.apply(person); //  Output: Yohan

    person.getName.call(anotherPerson); //  Output: Teghen
    person.getName.apply(anotherPerson); //  Output: Teghen
}, 10); 


person.getName.call(person); //  Output: Yohan
person.getName.apply(person); //  Output: Yohan

person.getName.call(anotherPerson); //  Output: Teghen
person.getName.apply(anotherPerson); //  Output: Teghen

Breakdown: `call` and `apply` both invoke getName with this explicitly set to either anotherPerson or person.
Thus, this.name accesses their name property.


Using closure and arrow functions

const person = {
    name: 'Yohan',
    getName: function() {
      function retrieveName() {
        console.log('With function declaration => ', this.name);
      }   
      retrieveName(); 
    }    
  };

person.getName(); // Output : With function declaration =>  undefined


// Fix using closure
const personWithClosure = {
    name: 'Yohan',
    getName: function() {   
        const that = this;     
        function retrieveName() {     
            console.log('\nWith Closure => ', that.name);
        }   
        retrieveName(); 
    }    
  };

  personWithClosure.getName(); // Output : With Closure =>  Yohan



// fix with arrow function
const personWithArrowFunction = {
    name: 'Yohan',
    getName: function() {               
        const retrieveName = () => {     
            console.log('\nWith Arrow function => ', this.name);
        }   
        retrieveName(); 
    }    
  };

  personWithArrowFunction.getName(); // Output : WWith Arrow function =>  Yohan

Brakdown: In the first case, the this context is lost inside the retrieveName function.

Using a closure (that = this) or an arrow function(uses `lexical this`) preserves the correct this context.


It must very clear at this point that, understanding how to manage this context is essential for writing reliable JavaScript code.

The next section will introduce object prototypes and how they play a crucial role in JavaScript inheritance.

Introduction

In JavaScript, every object has an internal property called `[[Prototype]]`, which is either another object or null.

This property serves as a link to another object from which properties and methods can be inherited.

The `[[Prototype]]` is typically accessed using the __proto__ property, although it is more commonly interacted with via the Object.getPrototypeOf() and Object.setPrototypeOf() methods.


The Role of Prototypes in JavaScript Object Inheritance

Prototypes play a critical role in JavaScript's inheritance system.

JavaScript uses a prototype-based inheritance model, where objects can inherit properties and methods from other objects.

When a property or method is accessed on an object, JavaScript first looks for the property or method on the object itself.

If it doesn't find it, JavaScript then looks up the prototype chain to see if it exists on the prototype.

This prototype-based inheritance allows for object composition and code reuse.

For example, if multiple objects share common behaviors, those behaviors can be defined on a prototype object that each of those objects inherits from.


The prototype Property of Functions

In JavaScript, functions (when used as `constructors`) have a special property called `prototype`.

This property is used to set the `[[Prototype]]` of objects created by that function when the function is used as a constructor (with the `new` keyword).

function Person(name, age) {
    this.name = name;
    this.age = age;
  }
  
  Person.prototype.getDetails = function() {
    return `${this.name} is ${this.age} years old.`;
  };
  
  const yohan = new Person('Yohan', 1);
  const lucky = new Person('Lucky', 2);
  
  console.log(yohan.getDetails()); // Output:  Yohan is 1 years old.
  console.log(lucky.getDetails()); // Output:  Lucky is 2 years old.

Breakdown: In this example, getDetails is defined on Person.prototype.

Both yohan and lucky have access to getDetails through the prototype chain.


Prototype Chain

The `prototype chain` is the mechanism by which JavaScript objects inherit properties and methods from other objects.

When an object property or method is accessed, JavaScript first looks for the property or method on the object itself.

If it is not found, JavaScript looks up the `[[Prototype]]` chain to see if it can find it on the prototype. T

his process continues until the end of the prototype chain is reached (ie when prototype lookup arrives at null).

const animal = {
    eat: function() {
      console.log('Eating...');
    }
  };
  
const dog = {
    bak: function() {
        console.log('baking...');
    }
};

const bird = {
    fly: function() {
        console.log('flying...');
    }
}

const fish = {
    swim: function() {
        console.log('swimming...');
    }
}

// update the prototype of each object to point to animal
Object.setPrototypeOf(dog, animal);
Object.setPrototypeOf(bird, animal);
Object.setPrototypeOf(fish, animal);

// now we can call the eat method on each object
dog.bak(); // Output : baking...
dog.eat(); // Output : Eating...

bird.fly(); // Output : flying...
bird.eat(); // Output : Eating...

fish.swim(); // Output : swimming...
fish.eat(); // Output : Eating...

Breakdown: In this example, dog, bird and fish inherits from animal.

The `eat` method is not defined on any of them, but it is found on animal, which is their updated prototype.

Thus, dog, fish and bird can use the eat method via the prototype chain.

Basically, To sum up:

  • Prototypes are objects from which other objects can inherit `properties` and `methods`.
  • Prototype-based inheritance allows objects to reuse `properties` and `methods` defined on other objects.
  • The prototype property of functions is used to set the `[[Prototype]]` of objects created by those functions.
  • The `prototype chain` is the sequence of objects that JavaScript follows when trying to resolve a property or method.


Creating and Using Prototypes

Prototypes can be created in various ways:

1. Using Constructor Functions:

Constructor functions allow you to create multiple instances of an object with shared properties and methods on the prototype.

function Account (id, name, initialAmount = 0) {
    this.id = id;
    this.name = name;
    this.balance = initialAmount;

}

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

Account.prototype.getAccountDetails = function getAccountDetails () {
    return `Hey ${this.name}!, Your Account ${this.id}, has a standing balance of $${this.balance}`;
}

Account.prototype.deposit = function (amount) {
    if (amount > 0) {
        this.balance += amount;
        console.log(`\nYou have successfully deposited ${amount}. You new balnce is ${this.balance}`);
    }    
}

Account.prototype.withdraw = function (amount) {
    if (amount > this.balance) {
        console.log('\nYour balance is insufficient!');
        return
    }
    this.balance -= amount;  
    console.log(`\nYou have successfully withdrawn ${amount}. You new balnce is ${this.balance}`);  
}

const myAccount = new Account(1010, 'Yohan Teghen', 10000);
const anotherAccount = new Account(2020, 'Lucky Teghen', 20000);

[myAccount, anotherAccount].forEach(account => {
    console.log(`\n\n\n ****************** Hello ${account.name}! Welcome to the Account Management System ******************`);
    console.log(account.getAccountDetails()); 
    console.log(account.getBalance()); 

    account.deposit(5500); 
    console.log(account.getAccountDetails()); 
    console.log(account.getBalance()); 

    account.withdraw(200000); 
    account.withdraw(13000); 

    console.log(account.getAccountDetails()); 
    console.log(account.getBalance()); 
}) ;
/** Outputs:
 ****************** Hello Yohan Teghen! Welcome to the Account Management System ******************
Hey Yohan Teghen!, Your Account 1010, has a standing balance of $10000
10000

You have successfully deposited 5500. You new balnce is 15500
Hey Yohan Teghen!, Your Account 1010, has a standing balance of $15500
15500

Your balance is insufficient!

You have successfully withdrawn 13000. You new balnce is 2500
Hey Yohan Teghen!, Your Account 1010, has a standing balance of $2500
2500


 ****************** Hello Lucky Teghen! Welcome to the Account Management System ******************
Hey Lucky Teghen!, Your Account 2020, has a standing balance of $20000
20000

You have successfully deposited 5500. You new balnce is 25500
Hey Lucky Teghen!, Your Account 2020, has a standing balance of $25500
25500

Your balance is insufficient!

You have successfully withdrawn 13000. You new balnce is 12500
Hey Lucky Teghen!, Your Account 2020, has a standing balance of $12500
12500 
 */

Breakdown: In this example, Account is a constructor function. All the are methods are defined on Account.prototype, so it is shared among all instances of Account.


2. Using Object.create:

The Object.create method allows you to create a new object with a specified prototype object.

const animal = {
    eat: function() {
      console.log('Eating...');
    }
  };
  

const dog = Object.create(animal);

dog.bak =  function() {
    console.log('baking...');
}


// now we can call the eat method on each object
dog.bak(); // Output : baking...
dog.eat(); // Output : Eating...

Breakdown: dog is created with animal as its prototype. The eat method is inherited from animal, and bark is defined directly on dog object.

Setting Properties and Methods on the Prototype

As we saw earlier in the account related example, properties and methods can be added to an object's prototype to ensure they are shared across all instances of that object.

This is easily achieved by the prototype object like so: Account.prototype.fn = function () {}

Accessing Properties and Methods through the Prototype Chain

When accessing a property or method on an object, JavaScript first looks for the property or method on the object itself.

If it doesn't find it, it looks up the prototype chain.

As we saw with the example on animals, the eat method is not found on dog itself, so JavaScript looks up the prototype chain and finds eat on animal, which is the prototype of dog.


Overriding prototype properties and methods

It's also possible to override a prototype's properties by defining a local property with same name directly on the instance object.

const animal = {
    eat: function() {
      console.log('Eating...');
    }
  };
  
const dog = Object.create(animal);

dog.eat = function () {
    console.log('Eating locally.....');
}

dog.eat(); // Output : Eating...


Creating and using prototypes effectively allows for more modular, reusable, and maintainable code.

1. Prototype Pattern

The prototype pattern is used to create objects based on a prototype object, allowing for the sharing of properties and methods.

const vehiclePrototype = {
    start: function() {
      console.log(`${this.type} started`);
    },
    stop: function() {
      console.log(`${this.type} stopped`);
    }
  };
  
  function Vehicle(type) {
    this.type = type;
  }
  
  Vehicle.prototype = vehiclePrototype; // vehicle.prototype -> vehiclePrototype -> Object -> null
  console.log(Vehicle.__proto__.__proto__.__proto__) // null (just to illustrate the prototype chain)

  const car = new Vehicle('Car');
  const bike = new Vehicle('Bike');
  
  car.start(); // Logs "Car started"
  bike.stop(); // Logs "Bike stopped"

Breakdown: vehiclePrototype defines shared methods, and Vehicle constructor assigns it as the prototype for new instances, allowing those instances to use start and stop methods.

2. Mixin Pattern

The mixin pattern is used to extend the functionality of a class by mixing in methods from another object.

const canfloat = {
    float: function() {
      console.log(`${this.name} floats...`);
    }
};
  
const canDive = {
    dive: function() {
        console.log(`${this.name} dives...`);
    }
};

function Vehicle(name) {
    this.name = name;
}

Vehicle.prototype.fly = function () {
    console.log(`${this.name} can't fly...`);
}
  
Object.assign(Vehicle.prototype, canfloat, canDive);

const submarine = new Vehicle('Submarine');


submarine.float(); // Output:  Submarine floats...
submarine.dive(); // Output  Submarine dives...
submarine.fly(); // Output Submarine can't fly...

Breakdown: The canDive and canFloat objects provide additional methods that are mixed into Person.prototype using Object.assign, allowing instances of Vehicle to use these methods.

Combining `this` with `prototypes` in JavaScript allows for powerful and flexible object-oriented programming.


Conclusion

In this guide, we explored the critical concepts of the `this` keyword and `object prototypes` in JavaScript, providing a comprehensive beginner-friendly understanding of their roles and usage.

Here's a quick recap:

  • We examined the definition and determination of this in various contexts, including global, function, method, constructor, and arrow functions.
  • We identified common issues related to losing this context and provided solutions using bind(), call(), and apply().
  • We defined prototypes, explained their role in JavaScript's inheritance model, and described the prototype chain.
  • We demonstrated how to create prototypes, set properties and methods on them, and access these through the prototype chain.
  • Finally, we showed how this works within methods defined on prototypes and provided some common design patterns that utilize these concepts.

Mastering the concepts of this and prototypes is essential for advanced JavaScript development.

These concepts form the backbone of object-oriented programming in JavaScript, enabling us to create complex, efficient, and maintainable applications.

Understanding how this context works and how to utilize prototypes effectively allows for better code reuse, cleaner design patterns, and more robust solutions.

Interested in exploring more 🤓? Check out these Related Posts.