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

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

# chaicode# javascript# webdev# programming
The Magic of `this`, `call()`, `apply()`, and `bind()` in JavaScriptPratham

How to take control of this — and borrow functions like a pro. In a previous article, I covered...

How to take control of this — and borrow functions like a pro.


In a previous article, I covered how this works in JavaScript — it refers to the object that is calling the function. Simple rule: look left of the dot.

But here's the thing that bugged me: what if I want to control what this refers to? What if I have a function that belongs to one object, but I want to use it with a different object? Am I supposed to copy-paste the function? Create a duplicate?

Nope. JavaScript gives you three methods to explicitly set this: call(), apply(), and bind(). They're like a remote control for this — you decide exactly what it points to.

These three methods were one of those topics in the ChaiCode Web Dev Cohort 2026 that seemed intimidating at first but turned out to be beautifully simple. Let me show you.


Quick Refresher: What this Means

Before we dive into call, apply, and bind, let's make sure the foundation is solid.

this refers to the object that is calling the function. Think of it as answering the question: "Who is running this code right now?"

const user = {
  name: "Pratham",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};

user.greet(); // "Hi, I'm Pratham"
// Who called greet()? → user → so this = user
Enter fullscreen mode Exit fullscreen mode

this Inside Regular Functions

When a function is called without an object (standalone), this defaults to the global object (or undefined in strict mode):

function sayName() {
  console.log(this.name);
}

sayName(); // undefined — no object is calling it
Enter fullscreen mode Exit fullscreen mode

this Inside Object Methods

When called as a method, this is the object:

const person = {
  name: "Pratham",
  city: "Delhi",
  introduce() {
    console.log(`${this.name} from ${this.city}`);
  },
};

person.introduce(); // "Pratham from Delhi"
Enter fullscreen mode Exit fullscreen mode

The Problem: What If I Want to Reuse This Function?

const pratham = {
  name: "Pratham",
  city: "Delhi",
  introduce() {
    console.log(`${this.name} from ${this.city}`);
  },
};

const arjun = {
  name: "Arjun",
  city: "Mumbai",
};

// Arjun doesn't have an introduce() method.
// Do I copy-paste the function? NO!
// I can BORROW it using call(), apply(), or bind().
Enter fullscreen mode Exit fullscreen mode

This is exactly the problem these three methods solve. Let's look at each one.


call() — Call a Function with a Specific this

call() lets you invoke a function immediately and explicitly set what this should be. You pass the desired this as the first argument, followed by any function arguments.

Syntax

functionName.call(thisArg, arg1, arg2, ...);
Enter fullscreen mode Exit fullscreen mode

Example: Borrowing a Method

const pratham = {
  name: "Pratham",
  city: "Delhi",
  introduce() {
    console.log(`${this.name} from ${this.city}`);
  },
};

const arjun = {
  name: "Arjun",
  city: "Mumbai",
};

// Borrow pratham's method and use it for arjun
pratham.introduce.call(arjun);
// "Arjun from Mumbai"
Enter fullscreen mode Exit fullscreen mode

What happened? We took pratham.introduce and called it, but told JavaScript: "Don't use pratham as this. Use arjun instead." So inside the function, this.name is "Arjun" and this.city is "Mumbai".

With Arguments

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

const user = { name: "Pratham" };

greet.call(user, "Hello", "!!");
// "Hello, I'm Pratham!!"

greet.call({ name: "Arjun" }, "Hey", ".");
// "Hey, I'm Arjun."
Enter fullscreen mode Exit fullscreen mode

Arguments after thisArg are passed to the function one by one, just like a normal function call.

Visual: How call() Works

greet.call(user, "Hello", "!!")
        ↓      ↓       ↓
      this   arg1    arg2

Inside greet():
  this = user           → this.name = "Pratham"
  greeting = "Hello"
  punctuation = "!!"

Result: "Hello, I'm Pratham!!"
Enter fullscreen mode Exit fullscreen mode

apply() — Same as call(), But Arguments in an Array

apply() does exactly the same thing as call(). The only difference: instead of passing arguments one by one, you pass them as an array.

Syntax

functionName.apply(thisArg, [arg1, arg2, ...]);
Enter fullscreen mode Exit fullscreen mode

Example

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

const user = { name: "Pratham" };

// call — arguments one by one
greet.call(user, "Hello", "!!");
// "Hello, I'm Pratham!!"

// apply — arguments as an array
greet.apply(user, ["Hello", "!!"]);
// "Hello, I'm Pratham!!"
Enter fullscreen mode Exit fullscreen mode

Same result. Same behavior. The only difference is how you pass the arguments.

When Is apply() Useful?

When your arguments are already in an array:

function introduce(greeting, role) {
  console.log(`${greeting}! I'm ${this.name}, a ${role}.`);
}

const user = { name: "Pratham" };
const args = ["Hey", "Full-Stack Developer"];

// apply is natural here — args is already an array
introduce.apply(user, args);
// "Hey! I'm Pratham, a Full-Stack Developer."
Enter fullscreen mode Exit fullscreen mode

A classic example — finding the max of an array:

const numbers = [5, 12, 8, 130, 44];

// Math.max doesn't accept arrays, but apply spreads them as arguments
const max = Math.max.apply(null, numbers);
console.log(max); // 130
Enter fullscreen mode Exit fullscreen mode

💡 Note: With modern JavaScript, you can use Math.max(...numbers) with the spread operator. But apply() was the way to do this before spread existed, and understanding it helps in interviews.


bind() — Create a New Function with a Fixed this

Here's where bind() differs from the other two. bind() does NOT call the function immediately. Instead, it returns a new function where this is permanently set to whatever you specify.

Syntax

const newFunction = functionName.bind(thisArg, arg1, arg2, ...);
// Later:
newFunction(); // this is permanently bound
Enter fullscreen mode Exit fullscreen mode

Example

const pratham = {
  name: "Pratham",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};

const arjun = { name: "Arjun" };

// bind() returns a NEW function — doesn't call it yet
const arjunGreet = pratham.greet.bind(arjun);

// Call it whenever you want
arjunGreet(); // "Hi, I'm Arjun"
arjunGreet(); // "Hi, I'm Arjun" — this is permanently bound
Enter fullscreen mode Exit fullscreen mode

With Arguments (Partial Application)

You can also pre-fill some arguments with bind():

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

const user = { name: "Pratham" };

// Pre-fill the greeting, leave punctuation for later
const sayHello = greet.bind(user, "Hello");

sayHello("!"); // "Hello, I'm Pratham!"
sayHello("?"); // "Hello, I'm Pratham?"
sayHello("."); // "Hello, I'm Pratham."
Enter fullscreen mode Exit fullscreen mode

bind() locked in this = user and greeting = "Hello". Now sayHello only needs the remaining argument.

The Most Common Use Case: Fixing Lost Context

Remember the problem where extracting a method loses this?

const user = {
  name: "Pratham",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};

// Extracting the method — context lost!
const greetFn = user.greet;
greetFn(); // "Hi, I'm undefined" ❌

// Fix it with bind()
const greetFn = user.greet.bind(user);
greetFn(); // "Hi, I'm Pratham" ✅
Enter fullscreen mode Exit fullscreen mode

This is especially useful with event handlers and callbacks:

const app = {
  name: "FeedPulse",
  start() {
    console.log(`${this.name} is starting...`);
  },
};

// Without bind — this is lost in setTimeout
setTimeout(app.start, 1000);
// "undefined is starting..." ❌

// With bind — this is preserved
setTimeout(app.start.bind(app), 1000);
// "FeedPulse is starting..." ✅
Enter fullscreen mode Exit fullscreen mode

call() vs apply() vs bind() — The Comparison

Feature call() apply() bind()
Calls function? ✅ Immediately ✅ Immediately ❌ Returns a new function
Sets this? ✅ Yes ✅ Yes ✅ Yes (permanently)
Arguments One by one As an array One by one (can pre-fill)
Syntax fn.call(obj, a, b) fn.apply(obj, [a, b]) fn.bind(obj, a, b)
Returns Function's return value Function's return value A new bound function
Best for Borrowing methods, one-time Array arguments, one-time Callbacks, event handlers

The Memory Trick

  • callComma separated arguments, Calls immediately
  • applyArray of arguments, Also calls immediately
  • bindBinds and returns, call it later (Be patient)

Function → Caller Relationship

Here's the complete picture of how this is determined:

┌─────────────────────────────────────────────────────────────────┐
│               WHO IS `this`?                                     │
├─────────────────────┬───────────────────────────────────────────┤
│                     │                                            │
│  obj.method()       │  this = obj (left of the dot)             │
│                     │                                            │
│  fn()               │  this = global (or undefined in strict)   │
│                     │                                            │
│  fn.call(obj)       │  this = obj (explicitly set)              │
│                     │                                            │
│  fn.apply(obj)      │  this = obj (explicitly set)              │
│                     │                                            │
│  fn.bind(obj)()     │  this = obj (permanently bound)           │
│                     │                                            │
│  () => {}           │  this = surrounding scope (inherited)     │
│                     │                                            │
│  new Constructor()  │  this = the new object                    │
│                     │                                            │
└─────────────────────┴───────────────────────────────────────────┘

Priority (highest to lowest):
  1. new
  2. bind
  3. call / apply
  4. obj.method()
  5. standalone fn()
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

Pattern 1: Method Borrowing

Use a method from one object on another without duplicating code:

const admin = {
  role: "admin",
  permissions: ["read", "write", "delete"],
  showAccess() {
    console.log(`${this.name} (${this.role}): ${this.permissions.join(", ")}`);
  },
};

const user = {
  name: "Pratham",
  role: "editor",
  permissions: ["read", "write"],
};

admin.showAccess.call(user);
// "Pratham (editor): read, write"
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Borrowing Array Methods

The classic — using array methods on array-like objects:

function logArgs() {
  // 'arguments' is array-like but NOT an array
  // Borrow Array's forEach to iterate it
  Array.prototype.forEach.call(arguments, (arg) => {
    console.log(arg);
  });
}

logArgs("JavaScript", "React", "Node.js");
// "JavaScript"
// "React"
// "Node.js"
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Binding Event Handlers

const counter = {
  count: 0,
  increment() {
    this.count++;
    console.log(`Count: ${this.count}`);
  },
};

// Without bind — this would be the button element, not counter
// document.querySelector("#btn").addEventListener("click", counter.increment.bind(counter));
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Partial Application with bind()

Create specialized functions from general ones:

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

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

console.log(double(5)); // 10
console.log(double(12)); // 24
console.log(triple(5)); // 15
console.log(triple(12)); // 36
Enter fullscreen mode Exit fullscreen mode

We don't care about this here (passed null), but we pre-fill the first argument. Now double always multiplies by 2 and triple always multiplies by 3.


Let's Practice: Hands-On Assignment

Part 1: Create an Object with a Method Using this

const developer = {
  name: "Pratham",
  stack: "MERN",
  describe() {
    console.log(`${this.name} is a ${this.stack} developer.`);
  },
};

developer.describe(); // "Pratham is a MERN developer."
Enter fullscreen mode Exit fullscreen mode

Part 2: Borrow That Method Using call()

const designer = {
  name: "Priya",
  stack: "UI/UX",
};

// Borrow developer's method for designer
developer.describe.call(designer);
// "Priya is a UI/UX developer."
Enter fullscreen mode Exit fullscreen mode

Part 3: Use apply() with Array Arguments

function buildProfile(role, experience) {
  console.log(`${this.name}${role} with ${experience} years of experience.`);
}

const user = { name: "Pratham" };
const args = ["Full-Stack Developer", 1];

buildProfile.apply(user, args);
// "Pratham — Full-Stack Developer with 1 years of experience."
Enter fullscreen mode Exit fullscreen mode

Part 4: Use bind() and Store the Function

const boundDescribe = developer.describe.bind({ name: "Arjun", stack: "Python" });

// Call it later — this is permanently set
boundDescribe();
// "Arjun is a Python developer."

// Even passing to setTimeout works
setTimeout(boundDescribe, 1000);
// (after 1 second) "Arjun is a Python developer."
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. this is determined by who calls the function. Look left of the dot. No dot = global or undefined.
  2. call() invokes the function immediately with a specified this and comma-separated arguments.
  3. apply() does the same as call(), but takes arguments as an array. Remember: Apply = Array.
  4. bind() does NOT call the function. It returns a new function with this permanently locked. Use it for callbacks and event handlers.
  5. All three methods let you borrow functions from one object and use them with another — without duplicating code.

Wrapping Up

call(), apply(), and bind() are the tools that give you full control over this. Instead of being at the mercy of JavaScript's calling context rules, you explicitly decide what this should be. Method borrowing, event handler binding, partial application — these patterns come up constantly in real-world code and interviews.

I'm working through all of this in the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Understanding call, apply, and bind was one of those "aha" moments where a bunch of confusing patterns suddenly made sense. If this has been frustrating you, I hope this article cleared things up.

Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way as the journey continues.

Happy coding! 🚀


Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode