Skip to main content

Command Palette

Search for a command to run...

The Magic of this, call(), apply(), and bind() in JavaScript

Published
9 min read
The Magic of this, call(), apply(), and bind() in JavaScript

By Saurabh Prajapati | Full-Stack Engineer at IBM India Software Lab


Why I Decided to Explore This

I'll be honest — for the longest time, this in JavaScript confused the heck out of me.

I'd write some code, it would work. I'd move the same function somewhere else, and suddenly this was undefined. I'd scratch my head, Google it, patch it with an arrow function, and move on without really understanding why.

Sound familiar?

After seeing call(), apply(), and bind() pop up again and again — in interview questions, open-source code, and MDN docs — I decided: okay, let's actually figure this out.

This blog is me documenting everything that finally made it click. Let's go.


1. What Is this in JavaScript?

Here's the simplest way to think about it:

this = "who is calling the function right now?"

this is not about where the function is written. It's about who is calling it at the moment it runs.

Think of it like a name tag that changes depending on who's wearing it.

function greet() {
  console.log("Hello, I am " + this.name);
}

If this is the window object, it looks for window.name.
If this is a user object, it looks for user.name.

Same function. Different this. Different result. That's the core idea.


2. this Inside Normal Functions

Let's start simple.

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

sayHello();

In a browser, this logs the window object (in non-strict mode).
In strict mode ("use strict"), this is undefined.

This tripped me up early on. I expected this to mean "the function itself." Nope. When no one specifically "calls" the function as a method, this defaults to the global object — or undefined in strict mode.

I wish I knew this earlier: this inside a plain function call doesn't refer to the function. It refers to whatever called it — often window by default.


3. this Inside Objects

Now it gets more interesting.

const developer = {
  name: "Saurabh",
  greet: function () {
    console.log("Hi, I'm " + this.name);
  },
};

developer.greet(); // Hi, I'm Saurabh

Here, developer is calling greet(). So this = developer. Makes sense, right?

But here's where it gets weird:

const greetFn = developer.greet;
greetFn(); // Hi, I'm undefined

Wait, what?

When you detach the function from the object and call it on its own, this no longer points to developer. It goes back to the global object (or undefined in strict mode).

This is the exact moment I thought — "Okay JavaScript, you're doing this on purpose to mess with me."

But it actually makes sense once you remember the rule: this depends on WHO is calling the function, not where the function lives.


4. What Does call() Do?

call() lets you manually set this when calling a function.

function greet(role) {
  console.log(`Hi, I'm \({this.name} and I work as a \){role}`);
}

const person = { name: "Saurabh" };

greet.call(person, "Software Engineer");
// Hi, I'm Saurabh and I work as a Software Engineer

Think of call() like saying:
"Hey function, run right now — and treat THIS object as your this."

Syntax

functionName.call(thisArg, arg1, arg2, ...);
  • thisArg → the object you want this to be

  • arg1, arg2, ... → normal function arguments, passed one by one

Real Use Case — Method Borrowing

const ibmDev = {
  name: "Saurabh",
  company: "IBM",
};

const visileanDev = {
  name: "Rahul",
  company: "Visilean",
};

function introduce() {
  console.log(`I'm \({this.name} from \){this.company}`);
}

introduce.call(ibmDev);     // I'm Saurabh from IBM
introduce.call(visileanDev); // I'm Rahul from Visilean

One function. Two different objects. call() makes it work for both.
This is called function borrowing — and it's actually super useful.


5. What Does apply() Do?

apply() is almost identical to call().

The only difference: instead of passing arguments one by one, you pass them as an array.

function greet(role, city) {
  console.log(`Hi, I'm \({this.name}, a \){role} from ${city}`);
}

const person = { name: "Saurabh" };

// call() — arguments one by one
greet.call(person, "Software Engineer", "India");

// apply() — arguments as an array
greet.apply(person, ["Software Engineer", "India"]);

Both produce the exact same output:

Hi, I'm Saurabh, a Software Engineer from India

When is apply() Useful?

It shines when your arguments are already in an array — like when using Math.max:

const scores = [85, 92, 78, 96, 88];

console.log(Math.max.apply(null, scores)); // 96

We pass null as thisArg because Math.max doesn't use this. We just need the array-spread behavior.

Honest take: With modern JavaScript, you can use the spread operator (...) instead of apply() in many cases. But knowing apply() helps you read older codebases — and it comes up in interviews all the time.


6. What Does bind() Do?

Here's where things get really interesting.

bind() doesn't call the function immediately. Instead, it returns a new function with this permanently bound to an object.

function greet() {
  console.log(`Hi, I'm ${this.name}`);
}

const person = { name: "Saurabh" };

const boundGreet = greet.bind(person);

// Call it whenever you want
boundGreet(); // Hi, I'm Saurabh
boundGreet(); // Hi, I'm Saurabh (still Saurabh, always Saurabh)

You get a function that remembers its this. Forever.

Real Use Case — Event Listeners

This is where bind() saves the day in React and vanilla JS:

const button = {
  label: "Click Me",
  handleClick: function () {
    console.log("Button clicked: " + this.label);
  },
};

// Without bind — 'this' would be the DOM element, not the button object
document.querySelector("button").addEventListener(
  "click",
  button.handleClick.bind(button)
);

I used this pattern a lot while building components at IBM. When you pass a method as a callback, it loses its this. bind() fixes that cleanly.

Pre-set Arguments with bind()

Here's a bonus feature I discovered — you can also pre-fill arguments:

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);

console.log(double(5));  // 10
console.log(double(9));  // 18

2 is always the first argument. You just supply the second. This pattern is called partial application — and it feels like magic once you get it.


7. The Difference — call vs apply vs bind

Here's the table I wish someone had shown me from the beginning:

Feature call() apply() bind()
Calls the function immediately? ✅ Yes ✅ Yes ❌ No
Returns a new function? ❌ No ❌ No ✅ Yes
How to pass arguments One by one As an array One by one (pre-set)
Use when You want to run now with a custom this Args are in an array You want to save for later

Simple memory trick I use:

  • callCall it now, Comma-separated args

  • applyApply it now, Array of args

  • bindBind it for later, returns a Bound function


8. Visualizing the Relationship

Here's how I think about it mentally:

Regular Function Call
─────────────────────
sayHello()
    └── this = window (or undefined in strict mode)


Object Method Call
──────────────────
developer.greet()
    └── this = developer ✅


call() / apply()
─────────────────
greet.call(customObj, args)
    └── this = customObj ✅ (runs immediately)


bind()
──────
const fn = greet.bind(customObj)
fn()
    └── this = customObj ✅ (runs later, always bound)

9. Hands-On Assignment — Try This Yourself!

Here's a small exercise I'd encourage you to do right now. Open your browser console or a CodeSandbox and follow along.

Step 1 — Create an Object with a Method

const developer = {
  name: "Saurabh",
  skills: ["React", "Node.js", "GenAI"],
  introduce: function (role, company) {
    console.log(
      `Hi! I'm \({this.name}. I work as a \){role} at ${company}.`
    );
    console.log(`My skills: ${this.skills.join(", ")}`);
  },
};

developer.introduce("Software Engineer", "IBM");

Step 2 — Borrow the Method with call()

const anotherDev = {
  name: "Priya",
  skills: ["Vue.js", "Python", "Docker"],
};

developer.introduce.call(anotherDev, "Backend Engineer", "Infosys");

Step 3 — Use apply() with Array Arguments

const args = ["Full-Stack Engineer", "Google"];

developer.introduce.apply(anotherDev, args);

Step 4 — Use bind() and Store the Function

const boundIntroduce = developer.introduce.bind(anotherDev);

// Use it anywhere, anytime
boundIntroduce("Frontend Engineer", "Flipkart");

// Still works!
setTimeout(boundIntroduce, 1000, "DevOps Engineer", "Microsoft");

Try changing the objects. Try passing different arguments. Break it and fix it — that's how it really sticks.


Key Discoveries from My Journey

A few things that genuinely surprised me:

  • Arrow functions don't have their own this. They inherit this from the surrounding scope. So call(), apply(), and bind() have no effect on them. Took me way too long to figure this out.

  • bind() is a lifesaver in React class components. That whole this.handleClick = this.handleClick.bind(this) in the constructor? Now I actually understand why it's there.

  • apply() was more useful before the spread operator. Now Math.max(...scores) does the same thing as Math.max.apply(null, scores). But apply() is still worth knowing.

  • this is runtime, not write-time. It's determined when the function is called, not when it's defined. That single realization fixed 80% of my this confusion.


How would you approach this? Do you have a better mental model for this?
Drop a comment or connect with me — I'd love to hear how you think about it!


About the Author

Saurabh Prajapati is a Full-Stack Software Engineer at IBM India Software Lab, where he builds cloud-native enterprise solutions for Maximo. He specializes in GenAI, React, and modern web technologies — and loves turning confusing concepts into approachable learning experiences.

More from this blog