Mastering this in JavaScript

A function mainly has two requirements to run - it’s lexical environment which holds all the parameters and variables present in the function and the object on which the function gets executed, which is what is referenced by this keyword. Both lexical environment and the this reference is contained in the execution context of a function which helps it in executing the code.
Regular functions often doesn’t require an object to execute on, they can sufficiently run on the passed parameters, but class methods usually require an object to perform the operations on and modify its state.
Let’s understand this with some examples:
function sum(a, b) {
const sum = a + b;
return sum;
}
const result = sum(1, 2);
console.log(result);
// Output: 3
Above example defines a simple utility function which calculates the sum of 2 numbers passed to it. It operates on these parameters and produces the result.
class Person {
constructor(name) {
this.name = name;
}
greet(){
console.log("Hello, ", this.name);
}
}
const user1 = new Person("User1");
const user2 = new Person("User2");
user1.greet();
user2.greet();
// Output:
// Hello, User1
// Hello, User2
Above code snippet defines a constructor and greet function both of which requires an object to execute on. This object is provided to the function dynamically during runtime which is known as this binding. Whichever object the function is called with, becomes this for that particular function call. Each function owns a stack frame - execution context - which holds the this reference for the function. This is how multiple objects share the same function.
Now that we know what this keyword and this binding is, let’s look at how binding is done. We know object is binded to a function call dynamically when function begins its execution but how does JS engine decide which object to bind, in short how does this gets assigned for various function calls? Well, that depends on how the function is called. Let’s take a look at at them one by one.
Default binding: When a regular function call is made, it gets binded to the global object for non-strict mode and undefined for strict mode, which basically makes sense as these functions are not intended to operate on any object.
function f() { console.log(this === globalThis); } f(); // Output: true function f() { "use strict" console.log(this); } f(); // Output: undefined
Implicit binding: When a function is called with an object using the (.) operator, it gets binded to that particular object.
function displayName() { console.log(this.name); } const obj = { name: "Demo" } obj.displayName = displayName; obj.displayName(); // Output: Demo
New binding: When constructor functions are called with new keyword, it creates a new object and binds them together.
function Person(name) { this.name = name } Person.prototype.displayName = function() { console.log(this.name); } const obj1 = new Person("Demo1"); obj1.displayName(); const obj2 = new Person("Demo2"); obj2.displayName(); // Output: // Demo1 // Demo2
For simplicity, lexical environments are omitted from the diagram but they hold reference to variables and functions present inside global and local scopes like Person, obj1, ob2, etc.
Explicit binding: Explicit binding can be used when you want to specify an object for binding yourself rather than relying on the implicit binding. JS provides 3 functions to do so:
Dot (.) operator also provides you the control to bind object to a function call but works only if the function is a property of the obejct(or present in its prototype chain).
call(): It is used to call a function on a given object which is passed as a parameter to the function. It takes first parameter as the object reference to which it needs to be binded, rest are the parameters to execute the function. Let’s look at an example to understand:
function setName(name) { this.name = name; } function Person(name, email) { setName(name); this.email = email; } const p1 = new Person("Demo", "demo@demo.com"); console.log(p1); console.log(globalThis.name); // Output: // Person { email: 'demo@demo.com' } // Demo
In above example, this inside Person points to the newly created obejct p1 as we learned in the new binding, so email is set correctly in the p1 object. But setName didn’t set the name in the p1 object as it is a regular call so this in setName points to the global obejct. So, how do we get access to the outer this object inside setName? One possible way is to use the Dot operator(this.setName()), but setName isn’t a peoperty of p1 object. This is where call() is used, when you want to outsource some functionality to another function which is not part of the current object but you want it to run on the same object without creating a new one or executing it on global.
function setName(name) { this.name = name; } function Person(name, email) { setName.call(this, name); this.email = email; } const p1 = new Person("Demo", "demo@demo.com"); console.log(p1); // Output: Person { name: 'Demo', email: 'demo@demo.com' }
Another example to understand it better:
function setName(name) { this.name = name; } const p1 = {}; setName.call(p1, "Demo"); // We can't do setName("Demo") as it will execute on global object // We can't do p1.setName("Demo") as setName is not available on p1 console.log(p1); // Output: { name: 'Demo' }
apply(): It does the same as call() except that it takes second argument as an array of parameters.
function setName(name, email) { this.name = name; this.email = email; } const p1 = {}; setName.apply(p1, ["Demo", "demo@demo.com"]); console.log(p1); // Output: { name: 'Demo', email: 'demo@demo.com' }
bind(): It returns a function binded with the passed object reference. So, whenever the function is called, it runs on that particular object irrespective of how it’s called.
function setName(name) { this.name = name; } const p1 = {}; const bindedSetName = setName.bind(p1); const p2 = {}; bindedSetName.call(p2, "Demo"); console.log(p1); console.log(p2); // Output: // { name: 'Demo' } // {}
This is mainly used for callbacks where function call is made by JS and you do not control how it is called and which binding it uses, so it’s better to bind it beforehand.
class Button { constructor(label) { this.label = label; } handleClick() { console.log(this.label); } } const btn = new Button('Click me'); setTimeout(btn.handleClick, 1000); // Output: undefined
class Button { constructor(label) { this.label = label; this.handleClick = this.handleClick.bind(this); } handleClick() { console.log(this.label); } } const btn = new Button('Click me'); setTimeout(btn.handleClick, 1000); // Output: Click me
Callbacks passes in event listeners gets binded to target obejct, while setTimeout gets binded to global(non-strict) or undefined(strict) or timeout object(Node env). To override this default behaviour, use bind().
Both call() and apply() are used for runtime binding while bind() is used during creation phase binding. bind() basically captures this reference in closure of the function it returns. So, whenever the function runs, this is taken from the closure and doesn’t depend on how the function is called. This concept is used in arrow functions implicitly which we will learn in later sections.
Arrow function binding: Arrow functions always gets binded to their outer environment irrespective of how they are called. They capture this reference from the time of definition. Therefore, they can’t be binded explicitly so call(), apply() and bind() doesn’t work on arrow functions. In fact, it tends to solve most of the above problems without the need of explicit binding, especially, in case of callbacks.
class Button { constructor(label) { this.label = label; } handleClick = () => { console.log(this.label); } } const btn = new Button('Click me'); setTimeout(btn.handleClick, 1000); // Output: Click me
But beware of this approach as arrow functions will add an instance property in the object and not on the prototype. Use it only when you can’t control how the function is called and you want to bind it to the outer scope like callbacks.
Super binding: In case of derived classes, it is necessary to call super constructor inside the derived class constructor before using this, so that super class properties can be set on the current object. Note that, when a method call is made using super, it binds the method with surrounding environment.
class Parent { constructor(type = "parent") { this.type = type; } display() { console.log(this); } } class Child extends Parent { constructor(name) { super("child"); this.name = name; } display() { super.display(); console.log(this) } } const child = new Child("Demo"); child.display(); const parent = new Parent(); parent.display(); /* Output Child { type: 'child', name: 'Demo' } Child { type: 'child', name: 'Demo' } Parent { type: 'parent' } */
Static methods binding: In case of static methods, this refers to the class object.
class Parent { constructor(type = "parent") { this.type = type; } display() { console.log(this); } static f() { console.log(this) } } const parent = new Parent(); parent.f() // parent.f is not a function Parent.f() // [class Parent]
this can hold a object reference as well as primitive in case of strict mode, for non-strict mode, it gets wrapped with the Wrapper class object of primitive.
Subscribe to my newsletter
Read articles from Payal Rathee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
