Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 008 / 100

'this' explained for JavaScript & TypeScript developers

Four rules decide what 'this' is.

Expona Admin8 min read

'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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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 this is set when a method is defined. It's set when called. A method can have different this values on different calls.
  • Forgetting that arrow functions in methods don't have this until the enclosing method is called. They look up the scope chain each time.
  • Using an arrow function when you specifically need this to be bound dynamically. If you need this to change based on how the function is called, avoid arrow functions.
  • Not realizing that bind creates a new function every time. If you call fn.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

  • this is determined by the call style, not where the function is defined. Remember the hierarchy: new beats call/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.method as 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 bind only 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles