this In Depth

Course 2 · Ch 4
this In Depth: call/apply/bind, and Arrow vs Regular Context
Finally answering, precisely, what this refers to in every situation — including the ones that break the simple rule

Fundamentals Chapter 5 and 7 both flagged this behaviour without fully resolving it: regular functions get their own this determined by how they're called; arrow functions inherit it from their surrounding scope. This chapter makes that precise, covers the situations where this goes wrong, and introduces three methods — call, apply, bind — that control it directly.

The Real Rule: this Depends on HOW a Function Is Called

const person = { name: "Philip", greet() { console.log(`Hi, ${this.name}`); } }; person.greet(); // "Hi, Philip" — called AS A METHOD on person const greetFn = person.greet; greetFn(); // "Hi, undefined" — called PLAIN, this is no longer person

The exact same function, called two different ways, gets a different this. person.greet() sets this to person, because it's called through person. Once that same function is assigned to greetFn and called on its own, there's no object to its left at the call site — this falls back to undefined (in strict mode/modules) rather than person.

This is exactly why passing a method as a callback breaks it
button.addEventListener("click", person.greet) (Fundamentals Chapter 8) hands the function over to be called plain later — by the time the browser actually calls it, this is no longer person. This is the single most common real-world "this" bug.

call() and apply() — Explicitly Setting this for One Call

function greet(greeting) { console.log(`${greeting}, ${this.name}`); } const person = { name: "Philip" }; greet.call(person, "Hello"); // "Hello, Philip" — args listed individually greet.apply(person, ["Hi"]); // "Hi, Philip" — args passed as an array

call and apply run a function immediately, with the first argument forced into the role of this for that one call — regardless of how the function was originally defined or attached. They're functionally identical except for how the remaining arguments are passed: call lists them one by one, apply takes them as a single array (similar in spirit to the spread operator from Intermediate Chapter 1).

bind() — Permanently Locking this for Later

const person = { name: "Philip", greet() { console.log(`Hi, ${this.name}`); } }; const boundGreet = person.greet.bind(person); boundGreet(); // "Hi, Philip" — this stays person, even called standalone button.addEventListener("click", boundGreet); // fixes the Chapter 8 callback bug above

Unlike call/apply, bind doesn't run the function immediately — it returns a brand-new function with this permanently locked to whatever was passed in, no matter how that new function later gets called. This is the standard fix for the callback problem above: bind the method to its object before handing it off as a callback.

Arrow Functions: No Own this, Inherited Instead

const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; // arrow function — "this" is inherited from start() console.log(this.seconds); }, 1000); } }; timer.start(); // logs 1, 2, 3... — "this" correctly stays "timer" throughout

Had the setInterval callback been a regular function instead, this inside it would be undefined (plain-call rule from earlier), breaking this.seconds++ entirely. The arrow function instead has no this of its own — it transparently uses whatever this was already in scope at the point it was written, which is timer, since start() itself was called as timer.start().

The practical rule of thumb from Fundamentals Chapter 5, now fully justified
Use a regular function for object methods that need their own this (set by however they're called). Use an arrow function for callbacks nested inside a method, specifically so they inherit the surrounding this instead of losing it.

Why Arrow Functions Can't Be Fixed with bind/call/apply

const arrowGreet = () => console.log(this.name); arrowGreet.call({ name: "Philip" }); // does NOT work — this is still inherited, ignores call entirely

Since an arrow function never has its own this to begin with, call/apply/bind have nothing to override — they're silently ignored for the this argument specifically. This is the one situation where those three methods simply don't apply.

MethodRuns immediately?Sets this for
fn.call(obj, a, b)YesThat one call, args listed individually
fn.apply(obj, [a, b])YesThat one call, args as an array
fn.bind(obj)No — returns a new functionEvery future call of the returned function

Coding Challenges

Challenge 1

Write a plain function introduce(role) that logs `${this.name} works as a ${role}`. Create two different objects, each with a name property, and use call() to invoke introduce with each object as this, passing a different role string each time.

📄 View solution
Challenge 2

Create an object counter with a count property (0) and a method increment() that increases count by 1 and logs it. Extract increment as a standalone function, use bind() to lock it to counter, then call the bound version three times via setTimeout to prove this stays correct even when called asynchronously.

📄 View solution
Challenge 3

Create an object stopwatch with elapsed: 0 and a method start() that uses setInterval with an ARROW function to increment elapsed and log it every second, for 3 seconds (then stop with clearInterval). Confirm elapsed actually increases by logging it.

📄 View solution

Chapter 4 Quick Reference

  • this depends on the call site — obj.method() sets this to obj; a plain call doesn't
  • fn.call(obj, a, b) — runs fn immediately with this = obj, args listed individually
  • fn.apply(obj, [a, b]) — same as call, but args passed as an array
  • fn.bind(obj) — returns a NEW function with this permanently locked to obj
  • Regular functions get their own this; arrow functions inherit this from their surrounding scope
  • call/apply/bind have no effect on an arrow function's this — there's nothing to override
  • Passing a method as a bare callback loses its this — bind it first, or use an arrow wrapper
  • Next chapter: error handling — try/catch/finally and custom Error classes