'this' explained. Four rules decide what 'this' is.
The this keyword in JavaScript is one of the most misunderstood concepts, yet it follows predictable rules once you understand the pattern. Every time a function runs, this is set based on how that function was called, not where it was defined. Master these four rules and you'll never be confused about what this points to again.
The Four Rules — How Call Style Determines 'this'
Before you can fix problems with this, you need to know the hierarchy that decides what it is. I'll rank them by specificity: the rule higher up wins if multiple apply.
| Call Style | What 'this' Is | Example |
|---|---|---|
| 1. new Fn() | The newly created object | new User() → this = new object |
| 2. fn.call(x) or fn.apply(x) | Whatever you pass as argument | greet.call(person) → this = person |
| 3. obj.method() | The object before the dot | user.getName() → this = user |
| 4. fn() | undefined (strict mode) or global (non-strict) | getName() → this = undefined |
Let me show you these in action. I'll start with the most common scenario you'll encounter in production code:
class User {
name: string = 'Alice';
// Rule 3: obj.method() — this = the object
greet() {
console.log(`Hello, I'm ${this.name}`);
}
}
const user = new User(); // Rule 1: new Fn() — this = the new object
user.greet(); // Rule 3: obj.method() — this = user
// Output: "Hello, I'm Alice"
const greetFn = user.greet;
greetFn(); // Rule 4: fn() — this = undefined (strict mode)
// Output: "Hello, I'm undefined" or throws error
Now let's look at call and apply. These are explicit ways to set this:
function introduce(greeting: string, emoji: string) {
console.log(`${greeting}, I'm ${this.name} ${emoji}`);
}
const person = { name: 'Bob' };
// Rule 2: fn.call(x) — this = x
introduce.call(person, 'Hi', '👋');
// Output: "Hi, I'm Bob 👋"
// fn.apply() is similar but takes an array of arguments
introduce.apply(person, ['Hey', '🙌']);
// Output: "Hey, I'm Bob 🙌"
The key insight: this is determined when the function is called, not when it's defined. The call style is everything.
Arrow Functions — No 'this' of Their Own
Arrow functions are different. They don't create their own this binding. Instead, they inherit this from the surrounding scope where they were defined. This behavior makes them perfect for callbacks and event handlers.
Let me show you the problem with regular functions first:
class Timer {
seconds: number = 0;
startRegular() {
setInterval(function() {
this.seconds++; // 'this' is undefined or global, not Timer
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.startRegular();
// Output: NaN, NaN, NaN... (because this.seconds is undefined)
Here's why it breaks: setInterval calls the function with no object context (Rule 4), so this becomes undefined in strict mode.
An arrow function fixes this because it looks up the scope chain and finds the this from the startRegular method call:
class Timer {
seconds: number = 0;
startArrow() {
setInterval(() => {
this.seconds++; // 'this' comes from the enclosing scope (Timer instance)
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.startArrow();
// Output: 1, 2, 3, 4... (correct!)
In React, arrow functions in class components prevent the "lost this" problem when passing handlers to child components. That's why you'll often see:
class Form {
name: string = '';
// Arrow function as class property — 'this' is always the Form instance
handleChange = (e: Event) => {
this.name = (e.target as HTMLInputElement).value;
};
}
Arrow functions are lexically bound. They capture this once, where they're written, and never change it.
Lost 'this' — The Callback Problem
One of the most common bugs I've seen in production is "lost this." It happens when you pass an object's method as a callback without preserving the context.
Here's a real scenario from a CRM system I worked on:
class NotificationService {
message: string = '';
userId: number = 0;
constructor(userId: number) {
this.userId = userId;
}
send() {
console.log(`Sending to user ${this.userId}: ${this.message}`);
}
scheduleSend(delayMs: number) {
// Bug: passing this.send as a callback
// When setTimeout calls it, 'this' is lost (Rule 4)
setTimeout(this.send, delayMs);
}
}
const notif = new NotificationService(42);
notif.message = 'Hello!';
notif.scheduleSend(1000);
// Output after 1s: "Sending to user undefined: undefined"
When setTimeout calls the callback, there's no object before the dot, so we hit Rule 4: this becomes undefined.
In a data processing pipeline with event emitters, I've seen this cause silent failures:
class DataProcessor {
processed: number = 0;
onDataReceived(data: unknown) {
this.processed++;
console.log(`Processed count: ${this.processed}`);
}
attachListener(emitter: EventEmitter) {
// If you pass the method directly, 'this' is lost when the event fires
emitter.on('data', this.onDataReceived); // Bug
}
}
The listener fires with no context, so this is undefined and the code fails silently or modifies the global scope.
Fixes — Three Practical Solutions
Once you recognize the problem, there are three main approaches to fix it. I'll rank them by how often I use them in production code.
1. Arrow Functions (Recommended)
This is the cleanest approach for most callbacks:
class NotificationService {
message: string = '';
userId: number = 0;
constructor(userId: number) {
this.userId = userId;
}
send() {
console.log(`Sending to user ${this.userId}: ${this.message}`);
}
scheduleSend(delayMs: number) {
// Arrow function captures 'this' from the method scope
setTimeout(() => this.send(), delayMs);
}
}
const notif = new NotificationService(42);
notif.message = 'Hello!';
notif.scheduleSend(1000);
// Output after 1s: "Sending to user 42: Hello!"
With event emitters:
class DataProcessor {
processed: number = 0;
onDataReceived(data: unknown) {
this.processed++;
console.log(`Processed count: ${this.processed}`);
}
attachListener(emitter: EventEmitter) {
// Arrow function preserves 'this'
emitter.on('data', (data) => this.onDataReceived(data));
}
}
2. .bind()
Use bind when you need to pass a bound function around or when defining properties:
class NotificationService {
message: string = '';
userId: number = 0;
constructor(userId: number) {
this.userId = userId;
// Bind in the constructor to avoid rebinding on every call
this.send = this.send.bind(this);
}
send() {
console.log(`Sending to user ${this.userId}: ${this.message}`);
}
scheduleSend(delayMs: number) {
// Now this.send is permanently bound
setTimeout(this.send, delayMs);
}
}
bind creates a new function with this permanently set. It's useful in class components or when you need the bound function multiple times.
3. Class Property with Arrow Function
This is what modern TypeScript often prefers in class definitions:
class NotificationService {
message: string = '';
userId: number = 0;
constructor(userId: number) {
this.userId = userId;
}
// Arrow function as a class property
// 'this' is bound to the instance automatically
send = () => {
console.log(`Sending to user ${this.userId}: ${this.message}`);
};
scheduleSend(delayMs: number) {
setTimeout(this.send, delayMs); // Works without issues
}
}
Each instance gets its own copy of the arrow function, so there's a small memory cost, but in practice it's negligible and the code is very readable.
Common Mistakes
- Assuming
thisis set when a method is defined. It's set when called. A method can have differentthisvalues on different calls. - Forgetting that arrow functions in methods don't have
thisuntil the enclosing method is called. They look up the scope chain each time. - Using an arrow function when you specifically need
thisto be bound dynamically. If you needthisto change based on how the function is called, avoid arrow functions. - Not realizing that
bindcreates a new function every time. If you callfn.bind(x)repeatedly, you get different function objects each time. Bind once in the constructor or as a class property. - Mixing arrow functions and regular functions inconsistently in the same class. Pick a style and stick with it for readability.
Key Takeaways
thisis determined by the call style, not where the function is defined. Remember the hierarchy:newbeatscall/apply, which beats method call, which beats bare function call.- Arrow functions don't have their own
this. They inherit it from the surrounding scope, making them ideal for callbacks. - Passing
obj.methodas a callback without preserving context is a common bug. Always use an arrow function, bind the method, or wrap it. - In production code, I prefer arrow functions in callbacks and class properties with arrow functions for event handlers. Use
bindonly when you need the explicit control or when working with older codebases. - If you're struggling with
this, consider whether you really need it. Sometimes a closure or pure function is simpler and less error-prone.






Comments
Loading comments…