Lesson 2 of 69

JavaScript Under the Hood

this Binding: call, apply, bind, and Arrow Functions

The this bug I see most often in interviews is not esoteric. It is this: a candidate writes a clean class, passes a method as a callback, and the method silently crashes because this is undefined. They stare at it for a few minutes and cannot explain why. This lesson explains exactly why, and gives you the mental model to never be surprised by this again.

The Core Rule: this Is Determined at Call Time

this is not a variable that you declare. It is a binding that is determined at the moment a function is called, not when it is defined. The only exception is arrow functions, which we will cover last.

There are four rules. They are applied in priority order.

flowchart diagram: this = newly created object

Rule 1: Default Binding

When a function is called as a plain function, not as a method and not with new, this defaults to the global object in sloppy mode or undefined in strict mode. TypeScript compiles with strict mode by default, so you will see undefined.

Typescript
function greet(): void {
  // In strict mode, this is undefined here
  console.log(this);
}

greet(); // undefined (strict mode)

This is the "nothing matched" case. It almost always indicates a bug.

Rule 2: Implicit Binding

When you call a function as a method, this is the object to the left of the dot.

Typescript
const user = {
  name: "Alice",
  greet(): void {
    console.log(`Hello, ${this.name}`);
  },
};

user.greet(); // Hello, Alice

The binding is implicit because there is nothing explicit in the call. The engine infers this from the call site. The function itself has not changed. Only the call site matters.

Here is where people get burned. If you assign the method to a variable and then call it, the object context is lost.

Typescript
const fn = user.greet;
fn(); // Hello, undefined: this is now undefined (strict mode)

You separated the function from the object. The function is now called as a plain function, so rule 1 applies.

Rule 3: Explicit Binding: call, apply, bind

You can force this to be whatever you want using call, apply, or bind.

call invokes the function immediately with a given this and individual arguments.

apply invokes the function immediately with a given this and arguments as an array.

bind returns a new function with this permanently bound. The new function can be called later.

Typescript
function introduce(greeting: string, punctuation: string): void {
  console.log(`${greeting}, I am ${this.name}${punctuation}`);
}

const person = { name: "Bob" };

// call: arguments listed individually
introduce.call(person, "Hi", "!"); // Hi, I am Bob!

// apply: arguments in an array
introduce.apply(person, ["Hey", "..."]); // Hey, I am Bob...

// bind: returns a new function, call it later
const boundIntroduce = introduce.bind(person, "Hello");
boundIntroduce("?"); // Hello, I am Bob?

bind is important in TypeScript because it is the classic fix for the callback this problem. We will see that shortly.

Rule 4: new Binding

When you call a function with new, JavaScript creates a fresh object, sets this to that object inside the constructor, and returns it.

Typescript
class Timer {
  private seconds: number;

  constructor(start: number) {
    this.seconds = start; // this = new Timer instance
  }

  getTime(): number {
    return this.seconds;
  }
}

const t = new Timer(10);
console.log(t.getTime()); // 10

Inside a constructor (or a constructor function), this always refers to the object being created. This is the most predictable case.

Arrow Functions: Lexical this

Arrow functions do not have their own this. They inherit this from the surrounding lexical scope at the time they are defined. The four rules above do not apply to arrow functions.

Typescript
class Counter {
  private count = 0;

  // Regular method: this depends on how it is called
  increment(): void {
    this.count++;
  }

  // Arrow method: this is always the Counter instance
  incrementArrow = (): void => {
    this.count++;
  };

  startRegular(): void {
    setTimeout(this.increment, 1000); // Bug: this is undefined when called
  }

  startArrow(): void {
    setTimeout(this.incrementArrow, 1000); // Works: this is still the Counter instance
  }

  startArrowInline(): void {
    setTimeout(() => {
      this.count++; // Works: arrow closes over this from startArrowInline
    }, 1000);
  }
}

The incrementArrow field is not on the prototype. It is created fresh for each instance. This is the trade-off: arrow methods avoid the this problem but use more memory because they are not shared via the prototype.

The Classic setTimeout Bug

This is one of the most common this bugs in JavaScript:

Typescript
class Poller {
  private data: string[] = [];

  fetchData(): void {
    // Simulating data fetch
    this.data.push("item");
    console.log(`Fetched ${this.data.length} items`);
  }

  startPolling(): void {
    // Bug: when setTimeout calls this.fetchData, it loses the Poller context
    setTimeout(this.fetchData, 1000);
  }
}

const poller = new Poller();
poller.startPolling(); // TypeError: Cannot read properties of undefined (reading 'data')

setTimeout holds a reference to the function, but not to the object. When it eventually calls the function, this is undefined (strict mode).

There are three fixes:

Typescript
class Poller {
  private data: string[] = [];

  fetchData(): void {
    this.data.push("item");
    console.log(`Fetched ${this.data.length} items`);
  }

  // Fix 1: Arrow function wrapper inline
  startWithArrowWrapper(): void {
    setTimeout(() => this.fetchData(), 1000);
  }

  // Fix 2: bind at the call site
  startWithBind(): void {
    setTimeout(this.fetchData.bind(this), 1000);
  }

  // Fix 3: Arrow class field (bind in the definition)
  fetchDataBound = (): void => {
    this.data.push("item");
    console.log(`Fetched ${this.data.length} items`);
  };

  startWithBoundField(): void {
    setTimeout(this.fetchDataBound, 1000);
  }
}

Fix 1 is the most common in modern code. Fix 2 is explicit and useful when you cannot change the method definition. Fix 3 is useful when the method is passed as a callback in many places.

Class Method Pitfall in React and Callbacks

The same pattern appears everywhere callbacks are used: event emitters, array methods, Promise handlers, React event handlers (when using class components).

Typescript
class EventProcessor {
  private processed = 0;

  // This will lose context when passed as a callback
  handleEvent(event: { type: string }): void {
    this.processed++;
    console.log(`Handled ${event.type}, total: ${this.processed}`);
  }

  // This will always have the right context
  handleEventBound = (event: { type: string }): void => {
    this.processed++;
    console.log(`Handled ${event.type}, total: ${this.processed}`);
  };
}

const processor = new EventProcessor();

const events = [{ type: "click" }, { type: "keyup" }];

// Bug: handleEvent loses context inside forEach
events.forEach(processor.handleEvent); // this is undefined

// Fix 1: bind explicitly
events.forEach(processor.handleEvent.bind(processor));

// Fix 2: arrow wrapper
events.forEach((e) => processor.handleEvent(e));

// Fix 3: use the bound arrow field
events.forEach(processor.handleEventBound);

call vs apply vs bind: When to Use Each

MethodCalls immediatelyArgumentsReturns
callYesSpread: fn.call(ctx, a, b)Return value of fn
applyYesArray: fn.apply(ctx, [a, b])Return value of fn
bindNoSpread (partial): fn.bind(ctx, a)New bound function

apply was useful before spread syntax for passing an unknown number of arguments. Today, fn.call(ctx, ...args) replaces most apply uses. bind is still the right choice when you need a reusable bound reference.

TypeScript and this Parameters

TypeScript lets you annotate this as a parameter in a function signature. This is a compile-time-only feature: the parameter is erased from the output. It lets TypeScript catch this misuse early.

Typescript
interface Greeter {
  name: string;
  greet(): void;
}

function greetUser(this: Greeter): void {
  console.log(`Hello, ${this.name}`);
}

const obj: Greeter = {
  name: "Alice",
  greet: greetUser,
};

obj.greet(); // OK
greetUser(); // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Greeter'

This catches the "method removed from context" bug at compile time rather than at runtime.

Key insight: this is not about where the function is defined. It is about how the function is called. Arrow functions are the exception: they capture this from their definition context and never change it. When you pass a method as a callback, always check whether the call site will preserve the object context.