Skip to content

Mastering function binding: a deep dive into .bind(), .call() and .apply() functions in Javascript and TypeScript

Posted on:November 19, 2023

In the universe of Javascript and TypeScript programming, functions stand at the heart of crafting elegant and scalable code. To navigate the depths of Javascript and TypeScript with the poise of a seasoned sensei, a developer must grasp the nuanced arts of function binding. Here, we unsheathe the revered .bind(), .call() and .apply() techniques, each wielding distinct powers to manipulate and direct your code’s execution, much like a martial artist controls their qi 🔥.

The essence of function binding

Venturing into .bind(), .call() and .apply(), let’s first meditate on the essence of function binding. In the dojo of JavaScript, binding enables a function to affix itself to a specific execution context. This maneuver dictates the value of this within a function’s body and paves the way to preset arguments.

Exploring the .bind() function

The first technique in our arsenal is the .bind() function. This function creates a new function with a specified execution context, allowing you to fix the value of this and, if needed, predefine arguments.

Syntax and usage of the .bind() function

Grasping .bind() is as straightforward as a front kick:

function greet(name: string) {
  console.log(`Hello, ${name}! My name is ${this.name}.`);
}

const ninja = {
  name: 'Kakashi',
};

const boundGreet = greet.bind(ninja, 'Naruto');

boundGreet(); // Output: Hello, Naruto! My name is Kakashi.

In this example, we bound the greet function to the ninja object and pre-defined the first argument as Naruto. When we invoke boundGreet(), the value of this will be the ninja object, and the pre-defined argument will be used.

Key differences between .bind() and regular function invocation

One key difference between .bind() and regular function invocation is that .bind() returns a new function with the specified execution context. This allows you to reuse the bound function multiple times. Regular function invocation, on the other hand, executes the function immediately without creating a new function.

Contrasting .bind() with regular invocations is akin to comparing a ready stance to a full attack. The .bind() function forges a new, initialized one while a direct call engages immediately.

Why employ .bind()?

The merits of .bind() are threefold:

  1. Ensuring this is tethered correctly.
  2. Presetting arguments to streamline future calls.
  3. Encouraging code reusability through customizable, bound function instances.

Common use cases and examples of .bind()

The .bind() function is widely used in scenarios where you want to ensure the execution context of a function or predefine arguments. For example, in event handlers, you can bind the execution context to the component instance to access its properties and methods seamlessly. Let’s take a look at an example:

class Button {
  constructor(private label: string) {}

  handleClick() {
    console.log(`Button "${this.label}" clicked!`);
  }
}

const button = new Button('Test button');

document.querySelector('#test-button')
  .addEventListener(
    'click', 
    button.handleClick.bind(button)
  );

In this example, we bind the Button class’ handleClick method to the button instance. When the button is clicked, the bound function is invoked, and the label of the button is logged to the console.

We could easily add the event listener in a method inside the class while switching the bind to the class’ this object:

class Button {
  constructor(private label: string) {}

  handleClick() {
    console.log(`Button "${this.label}" clicked!`);
  }

  addClickListener() {
    document.querySelector('#test-button')
      .addEventListener(
        'click', 
        this.handleClick.bind(this) // binding `this`
      );
  }
}

const button = new Button('Test button');

button.addClickListener()

Finally, we could push this further by using an arrow function as click handler and by removing the manual .bind(), like this:

class Button {
  constructor(private label: string) {}

  handleClick = () => { // using an arrow function
    console.log(`Button "${this.label}" clicked!`);
  }

  addClickListener() {
    document.querySelector('#test-button')
      .addEventListener(
        'click', 
        this.handleClick // removing manual bind to `this`
      );
  }
}

const button = new Button('Test button');

button.addClickListener()

Why does it work, you may ask? Because in Javascript arrow functions have a special property: they don’t have their own this context; instead they inherit the this context from the scope where they were defined, which happens to be the Button’s one in this example.

The .call() kata: instantaneous function invocation

Our next technique is the .call() function. Unlike .bind(), .call() immediately executes the function with the specified execution context and arguments, without returning a new function.

Syntax and mechanics of .call()

Engage .call() directly on a function as though launching a swift, direct strike:

function greet(location: string) {
  console.log(`Greetings from ${location}! My name is ${this.name}.`);
}

const ninja = {
  name: 'Jiraiya Sensei',
};

greet.call(ninja, 'the Land of Fire'); // Output: Greetings from the Land of Fire! My name is Jiraiya Sensei.

No intermediary — .call() executes greet with ninja as this, instantly.

Comparing .call() with .bind() and regular function invocation

While .call() and .bind() may seem similar, there is a key difference. .call() immediately executes the function, while .bind() returns a new function that can be invoked later. Regular function invocation, as we’ve seen before, executes the function right away.

.call() in practice

The .call() function allows you to explicitly define the execution context of a function and pass arguments directly, making it a flexible tool. It comes in handy when you want to reuse an existing function with different contexts and arguments.

One practical use case for .call() is when you want to borrow a method from an object and apply it to another object. Let’s say we have two objects, one with a talk method:

const ninja = {
  name: 'the Ghost of Tsushima 🥷',
  talk() {
    console.log(`I am ${this.name}!`);
  },
};

const samurai = {
  name: 'a mighty samurai, loyal to Lord Shimura 👺',
};

const ghostOfTsushima = ninja.talk();
// Output: I am the Ghost of Tsushima 🥷!
const ninjaInDisguise = ninja.talk.call(samurai); 
// Output: I am a mighty samurai, loyal to Lord Shimura 👺!

Here, the talk method, though born of the ninja, speaks through the samurai. We invoked the talk method of the ninja object with the samurai object as the execution context. This allows us to borrow the method and execute it on a different object.

Honorable mention 🏛️: the .apply() function — legacy code flexible argument array invocation

The final form, .apply(), mirrors .call() but embraces arrays, allowing for adaptable arguments in your function’s arsenal — but only when scrolling legacy scrolls 📜.

Syntax and usage of the .apply() function

Invoke .apply() with the same spirit as .call(), but arm it with an array of arguments:

const dragonBalls = [
  '4-star',
  '2-star',
  '6-star',
  '1-star',
  '5-star',
  '3-Star',
  '7-Star'
];

// check if all Dragon Balls are gathered
function areAllDragonBallsGathered(dragonBalls: string[]) {
  return dragonBalls.length === 7;
}

function summonDragon(
  ball1, 
  ball2, 
  ball3, 
  ball4, 
  ball5, 
  ball6, 
  ball7
) {
  console.log(
`The Dragon has been summoned with the power of ${ball1}, ${ball2}, ${ball3}, ${ball4}, ${ball5}, ${ball6}, and ${ball7}!

Now hurry up and make me immortal!`
  );
}

// check if all Dragon Balls are gathered before summoning the dragon
if (areAllDragonBallsGathered(dragonBalls)) {
// use apply to summon the dragon by passing the Dragon Balls as individual arguments
  summonDragon.apply(null, dragonBalls);
} else {
  console.log("You need all seven Dragon Balls to summon the dragon!");
}

In this example, we applied the summonDragon function to the dragonBalls array by passing its items as arguments.

It’s good to know about the .apply() method, as you may bump into it in legacy code. But keep in mind that, in modern Javascript, it can be replaced with the much simpler spread operator syntax (...). Check the refactored example below:

const dragonBalls = [
  '4-star',
  '2-star',
  '6-star',
  '1-star',
  '5-star',
  '3-Star',
  '7-Star'
];

function areAllDragonBallsGathered(dragonBalls: string[]) {
  return dragonBalls.length === 7;
}

// the spread operator can be used on arguments
function summonDragon(...args) {
  console.log(
`The Dragon has been summoned with the power of ${args.join(', ')}!

Now hurry up and make me immortal!`
  );
}

if (areAllDragonBallsGathered(dragonBalls)) {
  // refactor the apply function with the spread operator
  summonDragon(...dragonBalls)
} else {
  console.log("You need all seven Dragon Balls to summon the dragon!");
}

Best practices and tips for using .bind(), .call() functions effectively

To effectively use .bind(), .call() and .apply() in your TypeScript code, consider the following best practices:

  1. Use .bind() when you want to establish a new function with a fixed execution context, that is for pre-set function variants.
  2. Avoid excessive use of .bind(), as creating new functions can impact performance and tax the system.
  3. Utilize .call() when you want to immediately execute a function with a specified execution context and separate arguments.
  4. Always ensure that the execution context and arguments are appropriate for the function you’re working with.

Conclusion and final thoughts on leveraging .bind(), .call() and .apply()

Mastering function binding commands respect akin to a martial artist’s journey to mastery. The .bind(), .call() and .apply() functions are your scrolls of knowledge, enabling your code to adapt with grace and effectiveness. Employ them judiciously, mindful of their strengths and when restraint is warranted, and your path in Javascript and TypeScript will be marked by both precision and fluidity.

Review

What is function binding in JavaScript?

Function binding in JavaScript enables a function to affix itself to a specific execution context, dictating the value of this within a function’s body and paving the way to preset arguments.

What does the .bind() function do?

The .bind() function creates a new function with a specified execution context by fixing the value of this and, if needed, predefining arguments.

Provide the syntax and usage of the .bind() function in TypeScript.

Here’s an example of the .bind() function in TypeScript:

function greet(name: string) {
  console.log(`Hello, ${name}! My name is ${this.name}.`);
}

const ninja = {
  name: 'Kakashi',
};

const boundGreet = greet.bind(ninja, 'Naruto');

boundGreet(); // Output: Hello, Naruto! My name is Kakashi.
How does .bind() differ from regular function invocation?

.bind() returns a new function with the specified execution context for reuse, while regular function invocation executes the function immediately without creating a new function.

List the merits of using .bind().
  1. Ensures this is tethered correctly.
  2. Presets arguments to streamline future calls.
  3. Encourages code reusability through customizable, bound function instances.
Give an example of the .bind() function used in an event handler.

Here’s an example of .bind() used in an event handler:

class Button {
  constructor(private label: string) {}

  handleClick() {
    console.log(`Button "${this.label}" clicked!`);
  }
}

const button = new Button('Test button');

document.querySelector('#test-button')
  .addEventListener(
    'click', 
    button.handleClick.bind(button)
  );
What is the primary function of .call()?

The .call() function immediately executes the function with the specified execution context and arguments, without returning a new function.

Provide an example of using the .call() function in TypeScript.

Here’s an example of using the .call() function in TypeScript:

function greet(location: string) {
  console.log(`Greetings from ${location}! My name is ${this.name}.`);
}

const ninja = {
  name: 'Jiraiya Sensei',
};

greet.call(ninja, 'the Land of Fire'); // Output: Greetings from the Land of Fire! My name is Jiraiya Sensei.
Explain the difference between .call(), .bind(), and regular function invocation.

.call() executes the function immediately with a specified execution context and arguments, .bind() returns a new function for later invocation, and regular function invocation occurs right away without modifying the execution context.

What is a use case for .call()?

A common use case for .call() is borrowing a method from one object and applying it to another object with a different context.

Provide an example of borrowing a method using .call().

Here’s an example of borrowing a method using .call():

const ninja = {
  name: 'the Ghost of Tsushima 🥷',
  talk() {
    console.log(`I am ${this.name}!`);
  },
};

const samurai = {
  name: 'a mighty samurai, loyal to Lord Shimura 👺',
};

ninja.talk.call(samurai);
// Output: I am a mighty samurai, loyal to Lord Shimura 👺!
Describe the .apply() function and when it is typically used.

The .apply() function is similar to .call(), but it takes an array of arguments. It is typically used in legacy code and can be replaced with the spread operator (...) in modern JavaScript.

Show an example of how the .apply() function is used.

Here’s an example of using the .apply() function:

const dragonBalls = [
  '4-star',
  '2-star',
  '6-star',
  '1-star',
  '5-star',
  '3-Star',
  '7-Star'
];

function summonDragon(
  ball1, 
  ball2, 
  ball3, 
  ball4, 
  ball5, 
  ball6, 
  ball7
) {
  console.log(
`The Dragon has been summoned with the power of ${ball1}, ${ball2}, ${ball3}, ${ball4}, ${ball5}, ${ball6}, and ${ball7}!

Now hurry up and make me immortal!`
  );
}

summonDragon.apply(null, dragonBalls);
Give an example of how to replace .apply() with the spread operator in a refactored code.

Here’s an example of replacing .apply() with the spread operator in a refactored code:

const dragonBalls = [
  '4-star',
  '2-star',
  '6-star',
  '1-star',
  '5-star',
  '3-Star',
  '7-Star'
];

function summonDragon(...args) {
  console.log(
`The Dragon has been summoned with the power of ${args.join(', ')}!

Now hurry up and make me immortal!`
  );
}

summonDragon(...dragonBalls);
What are some best practices for using .bind(), .call() functions effectively?
  1. Use .bind() to establish a new function with a fixed context for pre-set function variants.
  2. Avoid excessive use of .bind() due to potential performance impact.
  3. Utilize .call() to immediately execute a function with specified context and arguments.
  4. Ensure appropriate execution context and arguments for the targeted function.