What you'll learn
Quick Answer
In JavaScript, this is determined by how a function is called, not where it is defined. There are four rules, checked in order: new binding sets this to the new object, explicit binding with call, apply or bind sets it to what you pass, method calls set it to the object before the dot, and everything else falls back to the global object or undefined in strict mode. Arrow functions ignore all four and inherit this from the surrounding scope, which is why they fix callback problems.
The Core Idea: Call Site, Not Definition Site
Most confusion about this comes from assuming it works like a variable — that a function written inside an object permanently belongs to it. It does not.
The value of this is decided at the moment the function is called, based on how it is called. The same function can have four different values of this depending on the call.
const user = {
name: 'Riya',
greet() { return `Hi, ${this.name}`; }
};
user.greet(); // "Hi, Riya" — called on user
const fn = user.greet;
fn(); // "Hi, undefined" — called on nothingNothing about greet changed. It was pulled out of the object and called plainly, so there is no object before the dot and this is no longer user.
Once you internalise "look at the call, not the definition", almost every confusing case becomes readable.
The Four Rules, In Priority Order
When a function runs, JavaScript checks these in order and the first match wins.
1. new binding. Called with new, this is the brand-new object being constructed.
function User(name) { this.name = name; }
const u = new User('Riya'); // this === the new object2. Explicit binding. Called via call, apply or bind, this is whatever you passed.
greet.call(user); // this === user
const bound = greet.bind(user); // permanently bound3. Implicit binding. Called as a method, this is the object immediately before the dot.
user.greet(); // this === user
app.data.user.greet(); // this === user, not appNote that only the last object before the dot matters, which surprises people with nested objects.
4. Default binding. None of the above, so this is the global object — or undefined in strict mode, which modules and class bodies use automatically. That is why the error is usually "cannot read property of undefined" in modern code rather than a silent global.
The Callback Trap
This is where this actually bites in real code. You pass a method somewhere as a callback, and it loses its object.
class Timer {
constructor() { this.seconds = 0; }
start() {
setInterval(function () {
this.seconds++; // TypeError: this is undefined
}, 1000);
}
}The function inside setInterval is not called as a method of the timer. It is called by the timer mechanism, with no object before a dot — so rule four applies and this is not your instance.
The same happens with event handlers, map callbacks, and any function passed by reference:
button.addEventListener('click', user.greet); // this === the button, not user
[1, 2].forEach(obj.process); // this === undefinedThree fixes exist. The modern one is an arrow function. The explicit one is bind. The old one, still worth recognising in legacy code, is capturing this in a variable:
const self = this; // the 'var self = this' pattern
setInterval(function () { self.seconds++; }, 1000);
How Arrow Functions Change Everything
Arrow functions do not follow the four rules at all. They have no this of their own — they inherit it from the surrounding scope at the moment they are defined, and nothing can change it afterwards.
class Timer {
constructor() { this.seconds = 0; }
start() {
setInterval(() => {
this.seconds++; // works: inherited from start(), which is the instance
}, 1000);
}
}This is why arrows are the default choice for callbacks. But the same property makes them wrong for object methods:
const user = {
name: 'Riya',
greet: () => `Hi, ${this.name}` // this is NOT user
};
user.greet(); // "Hi, undefined"The arrow was defined in the surrounding scope, not inside a function call, so it inherited whatever this was out there — typically the module scope or the global object. Object literals do not create a this binding.
Arrows also cannot be used with new, and ignore call, apply and bind entirely — you can pass a different this and it will be silently ignored.
The rule of thumb: arrow functions for callbacks, regular functions for methods.
How to Debug a this Problem
When this is wrong, work through it mechanically rather than guessing.
- Find the call site, not the definition. Where is the function actually invoked?
- Is it an arrow function? If so, ignore the call entirely and look at the enclosing scope where it was written.
- Otherwise apply the four rules in order. Was it called with
new? Withcall,applyorbind? With an object before the dot? If none, it is default binding. - Check for a lost reference. Passing
obj.methodanywhere detaches it, because you are passing the function, not the pair of object-and-function.
Two practical notes. In React class components, this is why constructors were full of this.handleClick = this.handleClick.bind(this) — and why class property arrow functions replaced that pattern.
And in a DOM event handler written as a regular function, this is the element that received the event, which is genuinely useful — until you switch it to an arrow function and it silently becomes the enclosing scope instead. Use event.currentTarget if you want the element regardless of function type.
