this — what it actually refers to, and why arrow functions are different
Unlike a normal variable, this isn't determined by where a function is DEFINED — it's determined by HOW the function is CALLED, a fundamentally different binding rule that's the root cause of nearly every confusing this bug.
3 min read
this is determined at call time, not definition time
const user = {
name: "Ada",
greet() {
return `Hi, I'm ${this.name}`;
},
};
user.greet(); // "Hi, I'm Ada" — called AS a method of user, so this = user
const detached = user.greet;
detached(); // "Hi, I'm undefined" — called with NO object before the dot, so this is NOT userThe exact same function, greet, produces two different results depending purely on how it was called — user.greet() calls it as a method, binding this to user; detached() calls it as a plain, unattached function, where this is undefined in strict mode (or the global object in non-strict mode) — nothing about greet's own definition changed at all between the two calls. This single fact — this is resolved by the call site, not the definition site — is the root cause of nearly every "why is this undefined/wrong here" bug in JavaScript.
The classic bug: losing this when passing a method as a callback
class Timer {
constructor() { this.seconds = 0; }
tick() { this.seconds += 1; }
}
const timer = new Timer();
setInterval(timer.tick, 1000); // BROKEN — tick is called as a plain function, this is NOT timersetInterval calls tick as a detached function — exactly like the detached() example above — so inside tick, this is not timer at all, and this.seconds += 1 either throws or silently does nothing useful depending on strict mode. This is an extremely common real bug: passing obj.method as a callback (to setInterval, an event listener, an array method) silently detaches the method from its intended this, because passing a function reference never carries its original call context along with it.
Arrow functions don't have their own this at all — they inherit it lexically
class Timer {
constructor() {
this.seconds = 0;
this.tick = () => { this.seconds += 1; }; // arrow function — this is captured from the SURROUNDING scope
}
}
const timer = new Timer();
setInterval(timer.tick, 1000); // WORKS — this.tick's `this` was fixed at DEFINITION time, in the constructorAn arrow function doesn't create its own this binding at all — it uses whatever this was in the enclosing (lexical) scope at the moment it was defined, permanently, regardless of how the arrow function itself is later called or passed around. Defining tick as an arrow function inside the constructor means it captures the constructor's this (the new instance) once, and that binding never changes no matter how tick is later called — which is exactly why arrow functions are the common, idiomatic fix for the "method loses this when passed as a callback" bug.
.bind(), .call(), and .apply(): manually controlling this
const boundTick = timer.tick.bind(timer); // returns a NEW function permanently bound to timer
setInterval(boundTick, 1000); // works, the older way — before arrow functions were common for this
greet.call(user, "hello"); // calls greet immediately, with this = user
greet.apply(user, ["hello"]); // same as .call, but arguments passed as an array.bind() returns a new function with this permanently fixed to whatever was passed, regardless of how that new function is later called — the pre-arrow-function solution to the same detached-method problem, still genuinely useful when a bound function needs to be created outside of a constructor context. .call() and .apply() do the same binding but invoke the function immediately rather than returning a new one, differing only in how they accept the remaining arguments (individually vs as an array) — all three exist specifically because this needs an explicit, deliberate mechanism to control, given that it's never simply "whatever object contains this code."
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What determines what `this` refers to inside a regular (non-arrow) function?
2. Why does `setInterval(timer.tick, 1000)` break, losing access to the timer instance's data?
3. Why do arrow functions fix the 'method loses this when passed as a callback' problem?