JavaScript this, call, apply and bind: How Is the Value of this Decided?
We learn how this is decided by the way a function is called, the lost method trap, the this of arrow functions, setting this with call, apply and bind, this in event handlers, and partial application with bind.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
An object, a method, and the same method copied into a variable:
const user = {
firstName: "Ada",
greet() {
return `Hello, I'm ${this.firstName}`;
}
};
const greet = user.greet;
console.log(user.greet());
console.log(greet());
Both lines call the same function. What does this print to the console? Make a guess before you read on. We'll give the answer right after we see how this is decided.
this Asks a Question: Who Called Me?
Think about the word "I." Written on a piece of paper by itself, it's not clear who it refers to; its meaning depends on who says the sentence. this works the same way. With the scope chain we saw earlier, variables are found based on where a function is written. this doesn't follow that rule: its value is decided at the moment the function is called, based on how it's called.
Right before running a function, the engine asks itself: "How was this function called?" Depending on the answer, it gives this a value. These are the cases you'll meet in practice. In a method call like obj.method(), this is the object to the left of the dot. In a plain fn() call, it's undefined in strict mode and the global object, window, in a classic script. With call, apply or bind, you decide what this is. An arrow function has no this of its own and uses the one around it. We'll see the this of a new call in the post on classes.
Method Calls: The Object Left of the Dot
Now back to the opening question. The first line prints "Hello, I'm Ada", because in the call user.greet() there's user to the left of the dot. The second line prints "Hello, I'm undefined" in the console. greet() is a plain call; there's nothing to the left of a dot. Since the console behaves like a classic script, this became window, and there is no window.firstName. Had the same code run in a module or in strict mode, this would be undefined and you'd get a TypeError.
The function is the same function; the only thing that changed is how it was called. To see this even more clearly, lend the method to another object:
const admin = { firstName: "Grace" };
admin.greet = user.greet;
console.log(admin.greet()); // "Hello, I'm Grace"
The method was written inside user but called through admin, so this is admin. A function doesn't remember who it belongs to; on every call, the engine looks to the left of the dot. With nested objects, the nearest dot wins: in shop.cart.total(), this is shop.cart, not shop.
The Lost Method Trap
Nobody writes the opening example on purpose, but the same thing happens every day in a much sneakier way: when you pass a method as a callback to another function. Paste this into the console and try it:
const timer = {
seconds: 0,
tick() {
this.seconds++;
console.log(this.seconds);
}
};
setTimeout(timer.tick, 100); // NaN
When you write timer.tick for setTimeout, you're handing over the function itself, not its connection to the object. A hundred milliseconds later, setTimeout calls it as a plain function. In that case the browser sets this to window, and since window.seconds is undefined, the ++ produces NaN. The method lost its owner along the way.
There are two common fixes. Either wrap the call in an arrow function so the method is again called with the object to the left of the dot, or use bind, which we'll see in a moment:
setTimeout(() => timer.tick(), 100); // 1
setTimeout(timer.tick.bind(timer), 200); // 2
The same problem shows up when you pass a method to map, to addEventListener or to a promise chain. The rule is simple: if you're passing a method somewhere without parentheses, know that its this isn't coming along.
The this of Arrow Functions
Arrow functions arrived with ES2015 and behave completely differently when it comes to this: they don't have their own. When this appears inside one, it looks at the surroundings of where the arrow function was written, just like an ordinary variable. In other words, in an arrow function this follows the lexical scope rules we saw earlier. That's exactly what we want when writing a callback inside a method:
const team = {
title: "Frontend",
members: ["Ada", "Alan"],
list() {
return this.members.map((m) => `${m} - ${this.title}`);
},
listBroken() {
return this.members.map(function (m) {
return `${m} - ${this.title}`;
});
}
};
console.log(team.list()); // ["Ada - Frontend", "Alan - Frontend"]
console.log(team.listBroken()); // ["Ada - undefined", "Alan - undefined"]
The arrow function in list takes this from list, and since list was called as team.list(), that's team. The ordinary function in listBroken, however, is called by map as a plain function, and this is lost. If you see a const self = this; line in older code, written to work around this problem, now you know why.
The flip side: writing an arrow function as an object's method is almost always a mistake.
const scoreBoard = {
score: 10,
show: () => this.score
};
console.log(scoreBoard.show()); // undefined
The this here doesn't belong to the object but to the surroundings where the object was written, that is, global scope. The rule in short: use the method shorthand for methods and arrow functions for the callbacks inside them.
call and apply: Handing Over this Yourself
Sometimes you want to call a function with a specific this. Every function has two methods for that. call uses its first argument as this and passes the rest to the function one by one. apply does the same but takes the arguments in an array:
function introduce(greeting, punctuation) {
return `${greeting}, I'm ${this.firstName}${punctuation}`;
}
const ada = { firstName: "Ada" };
const alan = { firstName: "Alan" };
console.log(introduce.call(ada, "Hi", "!")); // "Hi, I'm Ada!"
console.log(introduce.apply(alan, ["Hello", "."])); // "Hello, I'm Alan."
An easy way to remember: Apply goes with an Array; Call goes with Commas. Since spread arrived, apply isn't needed as much as it used to be. introduce.call(alan, ...args) does the same job, and the old Math.max.apply(null, numbers) is simply Math.max(...numbers) today.
bind: Locking this in Place
call and apply run the function immediately. bind doesn't; it returns a new function whose this is fixed for good. Wherever you pass that function afterwards and however you call it, this doesn't change:
const introduceAda = introduce.bind(ada);
console.log(introduceAda("Hey", "?")); // "Hey, I'm Ada?"
console.log(introduceAda.call(alan, "Hi", "!")); // "Hi, I'm Ada!", still Ada
That's what the timer.tick.bind(timer) fix in the lost method trap was doing. bind has one more trick: it also fixes any arguments you give after this. This is called partial application. You fill in some of a function's arguments ahead of time and produce a more specific function:
function calcPrice(taxRate, price) {
return price * (1 + taxRate);
}
const withVat = calcPrice.bind(null, 0.2);
console.log(withVat(100)); // 120
console.log(withVat(250)); // 300
const sayHiAsAlan = introduce.bind(alan, "Hi");
console.log(sayHiAsAlan("!!")); // "Hi, I'm Alan!!"
Since calcPrice doesn't use this, we passed null as the first argument. You can do the same with an arrow function such as (price) => calcPrice(0.2, price); both are valid, so go with whichever your team finds more readable. One warning: arrow functions ignore the this passed to call, apply and bind; their this can't be changed.
this in Event Handlers
When the addEventListener we saw earlier calls an ordinary function as a listener, it sets this to the element the listener is attached to. In an arrow function, the surrounding this applies as usual. Save this small HTML file, open it in your browser and click both buttons:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>this and events</title>
</head>
<body>
<button id="save">Save</button>
<button id="cancel">Cancel</button>
<script>
const saveBtn = document.getElementById("save");
saveBtn.addEventListener("click", function () {
console.log(this === saveBtn); // true
this.textContent = "Saved";
});
const orderForm = {
label: "Order form",
init() {
document.getElementById("cancel").addEventListener("click", () => {
console.log(`${this.label} cancelled`); // this is orderForm
});
}
};
orderForm.init();
</script>
</body>
</html>
In the first listener, this is the button itself. In the second, the arrow function takes this from init, so we can reach orderForm. You can also get the element through event.currentTarget instead of this; it works in arrow functions too and tells the reader more clearly what you mean.
Common Pitfalls
Code that works in the console and breaks in a module. The console and classic scripts aren't in strict mode by default; modules always are. In a plain call, this is window in the console and undefined in a module. The same code can print undefined in one place and throw a TypeError in the other.
Using a method directly as a listener. When you write button.addEventListener("click", cart.add), the this inside add is no longer cart but the button. Be careful when fixing it with bind, too: every call to bind creates a new function. removeEventListener("click", cart.add.bind(cart)) removes nothing, because it isn't the same function you added. Keep the bound function in a variable and use it both when adding and when removing.
An ordinary inner function inside a method. A helper written with function () { ... } inside a method doesn't inherit the outer method's this. Use arrow functions for callbacks.
Thinking this is the function itself. this doesn't point to the function it appears in but to the context of the call. If you want to reach the function itself, call it by its name.
A Small Experiment
★☆☆ Predict before running: const box = { size: 5, getSize() { return this.size; } }; const getSize = box.getSize; console.log(box.getSize(), getSize()); What do you see in the console, and what would happen in a module?
★★☆ Write a logger object with a prefix property and a log(message) method. Pass logger.log to setTimeout as is and watch it break. Then fix it in two separate ways, with an arrow function and with bind.
★★☆ Write an applyDiscount(percent, price) function and use bind to produce two new functions called discount10 and discount25. Predict the result of [100, 250, 80].map(discount10), then try it. Explain why there's no problem like the one with parseInt here, even though map also passes the index to the callback.
★★★ In a small HTML file, write a clicker object with count and increment(). Make increment a button's click listener, show the count on the button after every click, and remove the listener with removeEventListener after the fifth click.
In this post we used objects as structures that carry methods. But sometimes we use an object purely as a key-value store, and that's where we run into the limits of objects: keys get converted to strings, and finding out how many entries there are or looping over them takes extra work. In the next post we'll look at two collections designed for exactly this job, Map and Set, together with the new Set methods that came with ES2025.