PUBLISHED JUNE 29, 2024
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.
Prerequisite
Before jumping in, it is recommended that you have a basic understanding of:
- 1.JavaScript Fundamentals: Familiarity with JavaScript syntax, variables, functions, and basic control structures.
- 2.Objects in JavaScript: Basic knowledge of creating and manipulating objects.
- 3.JavaScript Functions: Understanding of function declarations, expressions, and arrow functions.
- 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 thewindow
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 thewindow
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.
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 usingbind()
,call()
, andapply()
. - ➢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.