Module 3 · Functions & Scope Mastery ⏱ 18 min

Function Context & this

By the end of this lesson you will be able to:
  • Determine what `this` refers to in different call contexts
  • Use `call`, `apply`, and `bind` to set `this` explicitly
  • Know that arrow functions do not bind their own `this`
  • Diagnose and fix the lost-`this` bug when a method is detached or passed as a callback

Every beginner hits the same baffling bug. You write an object with a method that reads this.count, call it once and it works, then hand that method to a callback or store it in a variable — and suddenly this is wrong, this.count is undefined, and your total collapses to NaN. Nothing crashed, the method is unchanged, and yet it broke.

The reason is the most misunderstood rule in JavaScript: this is not a property of the function. It is decided at the call site — by how the function is invoked — not by where it is written. Move the exact same function from one line to another and this changes underneath you. This lesson makes that rule predictable instead of magical: you will read a call site and know this before the code runs.

flowchart TD
  M["obj.method()"] --> T1["this = obj"]
  P["plain fn()"] --> T2["this = global/undefined"]
  A["arrow fn"] --> T3["this = outer this"]
  style M fill:#e0900b,color:#fff
  style P fill:#e0900b,color:#fff
  style A fill:#e0900b,color:#fff
The value of this depends on the call site, not the definition site.

Two call shapes, two values of this

What sits to the left of the dot is everything. When you write obj.greet(), JavaScript does two things at once: it fetches greet, and it sets this to obj for the duration of the call. The object the method came off becomes this.

Strip the object away and the rule collapses. A plain call — greet() with nothing before it — has no object to assign, so this falls back to the global object (globalThis), or to undefined under strict mode. That fallback is the whole reason the opening bug exists: peel a method off its object and the object stops being this.

Same function, called two ways. Watch what this becomes.
const user = {
  name: "Ada",
  greet() { return `Hi, I'm ${this.name}`; }
};

console.log(user.greet());           // "Hi, I'm Ada"  — this = user

const detached = user.greet;         // copy the function WITHOUT its object
console.log(typeof detached);        // "function" (the object is already gone)
// detached() would be a plain call: this = undefined, and this.name throws.

Train your eye on the call site

The practical skill is purely visual: scan left from the call. team.getName() has team before the dot, so this is team. getName() on its own has nothing there, so this is the fallback. render.call(card) overrides the scan — call hands the object over explicitly. Read calls this way and the surprises stop, because nothing about the function's definition decides this; only the moment of invocation does.

One wrinkle is worth naming. In sloppy mode a lost this becomes the global object, so this.count = 1 silently creates a global and your program limps on with no error. In strict mode the same line sets this to undefined and fails loudly. Prefer the loud failure: it turns silent data corruption into a readable stack trace.

The classic trap: a method that loses its object

Here is that opening bug, made concrete. const solo = obj.bump copies the function without its object. Calling solo() is now a plain call, so this is undefined, and a line like this.count += 1 reads undefined.count and throws — or, in sloppy mode, silently writes a stray global and your real counter never moves.

This trips people constantly because the method looks fine and works the first time. It only breaks the moment you detach it: pass it to setTimeout, assign it to an event property, or return it from another function. Anywhere the caller invokes it as a bare fn() instead of obj.fn().

flowchart TD
  M["obj.bump()"] --> R1["this = obj ✓ count rises"]
  P["const f = obj.bump<br/>f()"] --> R2["this = undefined ✗ throws"]
  style R1 fill:#1e7f3d,color:#fff
  style R2 fill:#b91c1c,color:#fff
Detach a method from its object and this loses its value at the call site.

Two reliable fixes

The arrow function fix is the most common. Define the body as an arrow and it captures this from where it was written — the enclosing method — instead of consulting the call site. Arrows ignore the call site entirely, which is exactly what you want inside a callback.

The bind fix is the explicit one. obj.bump.bind(obj) returns a brand-new function whose this is permanently locked to obj, no matter how it is later called. Reach for arrows when you control the definition; reach for bind when you must hand someone else's method into a callback.

An arrow callback keeps the outer this; bind locks this permanently.
const team = {
  lead: "Ada",
  members: ["Grace", "Linus"],
  // arrow inside map keeps this = team
  roster() { return this.members.map(m => `${m} led by ${this.lead}`); }
};
console.log(team.roster());

const counter = { count: 0, bump() { this.count += 1; return this.count; } };
const fixed = counter.bump.bind(counter);   // this locked to counter
console.log(fixed());                         // 1
console.log(fixed.call({ count: 99 }));       // 2 — bind wins, call can't override
flowchart LR
  C["fn.call(obj, a, b)"] --> N1["runs immediately"]
  A["fn.apply(obj, [a,b])"] --> N1
  B["fn.bind(obj)"] --> N2["returns a new fn<br/>run it later"]
  style N1 fill:#3776ab,color:#fff
  style N2 fill:#b45309,color:#fff
call and apply run now (comma vs array); bind returns a function to run later.
Exercise

What does this print? The method is called on the object, so this refers to that object.

const team = {
  name: "A-Team",
  getName() {
    return this.name;
  }
};
console.log(team.getName());
Exercise

Write createBoundGreet(obj) that returns a new function. When called, the returned function should call obj.greet() and return its result. Use bind to make sure this stays correct even if the returned function is stored and called later.

function createBoundGreet(obj) {
  // return a bound function
}
Exercise

getHandler is meant to return a function the UI can call later to bump this button's count. But it returns this.click unbound, so when the UI invokes the handler, this is wrong and the count never rises (or it throws). Repair getHandler so the returned function always bumps the right counter — use bind, or return an arrow.

function makeButton() {
  return {
    count: 0,
    click() { this.count += 1; return this.count; },
    getHandler() {
      return this.click;   // BUG: caller invokes it without the button
    }
  };
}
Exercise

Write invoke(fn, thisArg, argsArray) that calls fn with this set to thisArg and the arguments taken from argsArray. Use apply so a single array spreads into however many parameters the function expects. This is the transfer task: nothing in the lesson hands you an array of arguments, so you must pick the right tool yourself.

function invoke(fn, thisArg, argsArray) {
  // call fn with this = thisArg and arguments spread from argsArray
}
Exercise

What does this print? The arrow inside map does not make its own this, so it borrows this from tag() — which is box.

const box = {
  label: "tools",
  items: ["a", "b"],
  tag() {
    return this.items.map(i => `${i}:${this.label}`);
  }
};
console.log(box.tag());

Recap

  • this is set by the call site, not the definition. obj.m()this is obj; a bare m()this is globalThis (or undefined in strict mode).
  • Peeling a method off its object — const f = obj.m; f() — is the classic way to lose this.
  • Arrow functions inherit this from their enclosing scope and ignore the call site, which makes them ideal for callbacks.
  • bind returns a new function with this permanently fixed; call and apply run immediately (comma-separated args versus one array).
  • Arrow functions cannot be rebound — their this was sealed when they were written.

Next you'll see how functions can themselves accept other functions as arguments, which is the gateway to map, filter, and the rest of the array toolkit.

Checkpoint quiz

What is this inside a regular function call in strict mode?

Which method creates a new function with a permanently bound this value?

You write const f = obj.method and later call f(). What happened to this?

Go deeper — technical resources