DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Understanding 'this' in JavaScript

logotech

Demystifying this, bind, call, and apply: A Definitive Guide for Backend Specialists

In the universe of JavaScript and, by extension, Node.js, the concept of this can be a minefield for developers, especially when it comes to managing execution context in functions and class methods. The way this behaves can vary drastically depending on how a function is invoked, leading to subtle and hard-to-debug errors. Fortunately, the bind, call, and apply methods offer us precise control over this, allowing us to shape the execution context according to our needs.

The Mutable Nature of this

In JavaScript, the value of this is determined dynamically at the time a function is invoked. This means that this does not refer to the function itself, nor to the place where the function was defined, but rather to the object that invoked the function.

Consider the following simple example:

// Simple function
function greet() {
  console.log(`Hello, my name is ${this.name}`);
}

const person1 = { name: \"Alice\", sayHello: greet };
const person2 = { name: \"Bob\", sayHello: greet };

person1.sayHello(); // Output: Hello, my name is Alice
person2.sayHello(); // Output: Hello, my name is Bob
Enter fullscreen mode Exit fullscreen mode

In this case, when person1.sayHello() is called, this within greet refers to person1. Similarly, when calling person2.sayHello(), this refers to person2.

However, the situation becomes complicated when the function is passed as a callback or when there is no explicit object invoking it:

function greet() {
  console.log(`Hello, my name is ${this.name}`);
}

const person = { name: \"Charlie\" };

// Direct invocation of the function without an object
greet(); // Output: Hello, my name is undefined (in strict mode, it would throw an error)

// Passing the function as a callback
setTimeout(person.sayHello, 100); // Output: Hello, my name is undefined (or error)
Enter fullscreen mode Exit fullscreen mode

In the scenario above, this within greet refers to the global object (or undefined in strict mode) because the greet function was invoked without a specific context.

Mastering Context with call, apply, and bind

To resolve this ambiguity and ensure that this always points to the desired object, JavaScript provides us with three powerful methods: call, apply, and bind.

  1. call(): Invocation with Individual Arguments

The call() method invokes a function with a specific this value and arguments provided individually.

Syntax: function.call(thisArg, arg1, arg2, ...)

  • thisArg: The value to be passed as this to the invoked function.
  • arg1, arg2, ...: Individual arguments passed to the function.
function introduce(age: number, city: string): void {
  console.log(`Hello, I'm ${this.name}, I'm ${age} years old, and I live in ${city}.`);
}

const user = { name: \"Diana\" };

// Using call to set 'this' to 'user' and pass arguments individually
introduce.call(user, 30, \"São Paulo\");
// Output: Hello, I'm Diana, I'm 30 years old, and I live in São Paulo.
Enter fullscreen mode Exit fullscreen mode
  1. apply(): Invocation with an Array of Arguments

The apply() method is similar to call(), but it accepts the function's arguments as an array.

Syntax: function.apply(thisArg, [argsArray])

  • thisArg: The value to be passed as this to the invoked function.
  • argsArray: An array whose elements will be passed as arguments to the function.
function describe(hobbies: string[], occupation: string): void {
  console.log(`My hobbies are: ${hobbies.join(', ')}. My occupation is ${occupation}.`);
}

const developer = { name: \"Ethan\" };

const devHobbies = [\"programming\", \"reading\", \"traveling\"];
const devOccupation = \"Software Engineer\";

// Using apply to set 'this' to 'developer' and pass arguments as an array
describe.apply(developer, [devHobbies, devOccupation]);
// Output: My hobbies are: programming, reading, traveling. My occupation is Software Engineer.
Enter fullscreen mode Exit fullscreen mode
  1. bind(): Creating a New Function with a Fixed Context

Unlike call and apply, which invoke the function immediately, bind() creates a new function with this permanently bound to a specific value. Arguments can also be pre-defined.

Syntax: function.bind(thisArg, arg1, arg2, ...)

  • thisArg: The value to be bound as this in the new function.
  • arg1, arg2, ... (optional): Pre-defined arguments that will be passed to the function when it is invoked.
function displayProduct(price: number): void {
  console.log(`Product: ${this.productName}, Price: $${price.toFixed(2)}`);
}

const gadget = { productName: \"Smartphone XYZ" };

// Creating a new function 'showGadgetPrice' with 'this' bound to 'gadget'
const showGadgetAndPrice = displayProduct.bind(gadget);

// Invoking the new function with the 'price' argument
showGadgetAndPrice(1299.99);
// Output: Product: Smartphone XYZ, Price: $1299.99

// Bind with pre-defined arguments
function multiply(factor: number): number {
    return this.value * factor;
}

const numberContext = { value: 10 };

const multiplyByFive = multiply.bind(numberContext, 5); // 'factor' is already 5

console.log(multiplyByFive()); // Output: 50
Enter fullscreen mode Exit fullscreen mode

this in Classes and Methods

In classes, this typically refers to the class instance. However, when passing class methods as callbacks, the original context can be lost, requiring the use of bind.

class Counter {
  count: number = 0;

  increment(): void {
    this.count++;
    console.log(`Count: ${this.count}`);
  }
}

const myCounter = new Counter();

// Problem: 'increment' loses its 'this' context when passed as a callback
// setTimeout(myCounter.increment, 1000); // Would call increment without the correct context

// Solution 1: Use bind in the constructor
class CounterWithBind {
  count: number = 0;

  constructor() {
    // Permanently binds the 'increment' method to the 'this' instance
    this.increment = this.increment.bind(this);
  }

  increment(): void {
    this.count++;
    console.log(`Count (Constructor Bind): ${this.count}`);
  }
}

const myCounterWithBind = new CounterWithBind();
setTimeout(myCounterWithBind.increment, 1500); // Now works correctly

// Solution 2: Use bind at the time of passing (less common in constructors)
const myCounter2 = new Counter();
setTimeout(myCounter2.increment.bind(myCounter2), 2000); // Binds at the time of call
Enter fullscreen mode Exit fullscreen mode

Conclusion

Understanding and controlling this is fundamental to writing robust and predictable JavaScript code, especially in complex backend architectures. call, apply, and bind are indispensable tools in our arsenal, allowing us to manage execution context with precision. While call and apply are useful for immediate invocations with different ways of passing arguments, bind excels at creating functions with a fixed context, ideal for callbacks and design patterns where the context should be immutable. Mastering these techniques not only prevents common errors but also elevates the quality and maintainability of your backend code.

Top comments (0)