Mastering the Context: A Guide to this, call, apply, and bind
Understanding how JavaScript determines ownership and controls function execution

JavaScript was first designed to work with objects to group related data together. The problem arrived when functions were written to make work with the object.
Consider building an app that greets the user whenever they open the app, and it should work with every user. To implement functions, either it should be duplicated inside every object. which means maintaing multiple copies, or object should be passed as an argument every time the function is called, making this a exhausting process.
Neither option keeps the code clean. To solve this problem this keyword was introduced. It defined who the current owner is and attach the context whoever is calling the function. this gets set at the time a function is called, not when its written, and may result in losing the value on context when a function is passed around.
That is where call(), apply(), and bind() come in. They were added to the language specifically to give developers control over this.
What is this ?
this keyword provides a way to know who called them, and the method and values they contain. When a function is called, this to point to the object that made the call.
Example:
this in Regular Functions
When you write a plain function and call it on its own, without any object in front of it, this gets set to the global object (global in Node, window in browser). In strict mode, it becomes undefined.
function whoAmI() {
console.log(this);
}
whoAmI(); // window (browser), global (Node) or undefined (strict mode)
The function is called without any object in front, so this points to the global object and nothing meaningful.
this Inside Objects
When a function is defined inside an object and called through that object, this refers to the object itself.
const user = {
name: "User",
greet: function() {
console.log("Hi, I'm " + this.name);
}
};
user.greet(); // "Hi, I'm User"
Because user.greet() is called with user as the owner, this points to user, and this.name resolves to "User". Change the caller, and this changes with it.
Why It Breaks When You Copy A Method
Then a method is copied outside the object, stored in a variable, and it is called. this loses the context.
const user = {
name: "User",
greet: function() {
console.log("Hi, I'm " + this.name);
}
};
const detached = user.greet;
detached(); // "Hi, I'm undefined"
You copied the function reference, but this did not come along. When detached() runs, there is no object in front of it, so this is either global or undefined, and this.name comes back as undefined. The same problem happens when you pass an object method into setTimeout or an event listener.
This problem is solved by call(), apply(), and bind().
call(): Borrow a Function
call() invokes a function immediately and allows setting the context for that invocation by adding another argument.
// SYNTAX
call(thisArg)
call(thisArg, arg1, arg2,...,argN)
// Example
const user = {
name: "User",
greet: function() {
console.log("Hi, I'm " + this.name);
}
};
const anotherUser = { name: "Tom" };
// detached() broke because this was lost.
// call() fixes it by telling greet exactly what this should be.
user.greet.call(anotherUser);
// "Hi, I'm Tom"
// You can call it for any object you want
user.greet.call({ name: "Bob" });
// "Hi, I'm Bob"
Taking the same user object from above, we can borrow its greet method and run it for a completely different object.
apply(): call() With Array Arguments
apply() performs exactly like call(), the only difference is that we can pass multiple arguments in the form of an array instead of passing them one by one. The array gets unpacked into the function's parameter.
Take a function that greets a user with a custom message and a language tag. You want to run the same function for two different users, and the arguments you want to pass are already sitting in an array:
// SYNTAX
apply(thisArg)
apply(thisArg, argsArray)
// example
function greet(message, language) {
console.log(message + ", " + this.name + " [" + language + "]");
}
const user1 = { name: "user1" };
const user2 = { name: "user2" };
const args = ["Hello", "EN"];
// call: you pass the args one by one
greet.call(user1, "Hello", "EN"); // "Hello, user1 [EN]"
// apply: you pass the args as an array, same result
greet.apply(user1, args); // "Hello, user1 [EN]"
greet.apply(user2, args); // "Hello, user2 [EN]"
The args is an array which could have been received from API response, form input, or another function output.
greet.call(user1, ...args). This can replace apply().
bind(): Lock The Context For Later
Unlike call() and apply(), bind() does not run the function at all. It creates and returns a brand new function with this permanently locked in, along with the arguments you passed, and this function can be called when needed.
bind() can be understood using:
const user = {
name: "User",
greet: function() {
console.log("Hi, I'm " + this.name);
}
};
// The bug: this is lost when greet is detached
const detached = user.greet;
detached(); // "Hi, I'm undefined"
// The fix: bind locks this to user permanently
const fixed = user.greet.bind(user);
fixed(); // "Hi, I'm User"
// Same fix for setTimeout — passes the function, not the result
setTimeout(user.greet, 1000); // "Hi, I'm undefined"
setTimeout(user.greet.bind(user), 1000); // "Hi, I'm User"
Do not use setTimeout(user.greet(), 1000) with the parentheses included. That calls greet immediately and hands its return value, which is undefined, to setTimeout instead of scheduling the function itself. When passing functions as callbacks, leave the parentheses off, or use bind().
Another example is:
function log(...args) {
console.log(this, ...args);
}
const boundLog = log.bind("this value", 1, 2);
const boundLog2 = boundLog.bind("new this value", 3, 4);
boundLog2(5, 6);
// "this value", 1, 2, 3, 4, 5, 6
Let's understand it step by step:
Step 1: the first bind() - log.bind("this value", 1, 2) creates boundLog. It locks this to "this value" and fills the first two arguments as 1 and 2. Those arguments will always be passed first, no matter how boundLog is called later.
Step 2: trying to re-bind - boundLog.bind("new this value", 3, 4) tries to create a new function with a different this. Once this is locked by bind(), it cannot be changed, and the "new this value" gets ignored, because the bind() created a wrapper for boundLog, and when you try to create a new wrapper which try to provide new this gets ignored. Although, the value 3 and 4 gets added to the argument list.
Step 3: calling boundLog2(5, 6) - When boundLog2(5, 6) runs, the arguments arrive in the order they were locked in across all the bind calls, followed by whatever you pass at call time. That is why the output is "this value", 1, 2, 3, 4, 5, 6.
This behaviour is known as partial application.
call() vs apply() vs bind()
| Feature | call() |
apply() |
bind() |
|---|---|---|---|
| Runs immediately | yes | yes | no |
| Returns | result of the function | result of the function | a new bound function |
| How args are passed | one by one: (a, b) | as an array: ([a, b]) | one by one, at bind or call time |
| Can call again later | no, already ran | no, already ran | yes, stored as a function |
| Best used for | borrow and run now | run now with array args | callbacks, event handlers, timers |
Logically, this should depend on where the function is written. Instead, it depends on who calls the function at the moment it runs. This information helps to understand the behaviour of this in a better way.
Functionally, both call() and apply() are ways to execute a function using a given this, and the only difference is whether the arguments are provided individually or all as an array. Each method behaves differently from the other; bind() is a completely different concept since it returns a new function with a predefined context to call whenever you desire. This makes bind() the best option for use with callbacks or any other code that is related to the event.
REFERENCES:




