For a regular function, this is not fixed when the function is written — it’s decided every time the function is called, based on how it’s called. Four rules, checked in priority order:
new—thisis the brand-new object being constructed.- Explicit —
call/apply/bindsetthisto the object you pass. - Implicit — called as
obj.method(),thisisobj(the thing left of the dot). - Default — a plain
fn()call:thisisundefinedin strict mode (or the global object in sloppy mode).
Precedence: which rule wins
When more than one rule could apply, they’re checked in this order (highest first):
newbeats everything.new (fn.bind(obj))()ignores the boundobj—thisis the new instance.- Explicit (
call/apply/bind) beats implicit.obj.method.call(other)usesother. - Implicit (
obj.method()) beats default. - Default is the fallback.
Arrow functions sit outside this list entirely: they have no own this, so none of the four rules touch them — this comes from the enclosing lexical scope.
console.log. Async logs (setTimeout/Promise) appear in real execution order.The “lost this” trap
console.log. Async logs (setTimeout/Promise) appear in real execution order.The function didn’t change — only the call site did. user.greet() has an object to the left of the dot; fn() does not.
call vs apply vs bind
All three set this explicitly. The difference is how arguments are passed and whether it runs now:
call(thisArg, a, b)— invoke now, args listed individually.apply(thisArg, [a, b])— invoke now, args as an array.bind(thisArg, a)— return a new function withthis(and any args) permanently fixed; call it later.
function intro(greeting) { return `${greeting}, I'm ${this.name}`; }
intro.call({ name: 'Bo' }, 'Hey'); // "Hey, I'm Bo"
intro.apply({ name: 'Bo' }, ['Hey']); // "Hey, I'm Bo"
const f = intro.bind({ name: 'Bo' }); // f('Hey') → "Hey, I'm Bo"
bind also supports partial application — any args after thisArg are pre-filled, and more can be supplied at call time.
Implement bind from scratch
A classic whiteboard question. The key subtleties: forward both pre-bound and call-time args, and handle the new case (where the bound this must be ignored in favor of the new instance).
console.log. Async logs (setTimeout/Promise) appear in real execution order.Arrow functions have no this
An arrow function does not get its own this. It closes over the this of the enclosing lexical scope — and that binding can never be changed by call/apply/bind or by how it’s called. That’s exactly why arrows are perfect for callbacks inside methods:
console.log. Async logs (setTimeout/Promise) appear in real execution order.{ greet: () => this.name } breaks, because the arrow captures the outer this (often the module/undefined), not the object. Use a normal method for object methods; use arrows for callbacks inside them.this in classes
Class bodies are always strict mode, so a method that loses its receiver gives undefined (not the global object), and reading a property off it throws. This bites every React class component:
class Button {
constructor() { this.label = 'OK'; }
handleClick() { return this.label; } // regular method
}
const b = new Button();
const onClick = b.handleClick;
onClick(); // TypeError: Cannot read properties of undefined
Three standard fixes:
// 1. bind in the constructor
constructor() { this.handleClick = this.handleClick.bind(this); }
// 2. class field with an arrow (captures the instance's this)
handleClick = () => this.label;
// 3. bind at the call site
<button onClick={() => b.handleClick()} />
Arrow class fields (#2) are the most common in React class components because each instance gets a method permanently bound to it.
Strict vs sloppy mode
In a plain fn() call, default binding differs by mode:
function show() { return this; }
show(); // strict: undefined | sloppy: globalThis (window)
Sloppy mode also boxes primitive thisArgs: fn.call('x') gives this === new String('x') (an object), whereas strict mode keeps it the primitive 'x'. Modules and class bodies are always strict, so modern code almost always sees undefined.
What’s the output? (puzzles)
Puzzle 1 — detached method:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: first call throws (default binding → this is undefined, reading .val throws); obj.getVal() prints 10 (implicit binding).
Puzzle 2 — arrow vs method this:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: false then true. The arrow ignores the call site and captures the surrounding this (not counter); the regular method uses implicit binding.
Puzzle 3 — this in a nested function:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: listBroken throws (the plain callback is called by map with no receiver → default this); listFixed returns ['Eng:a','Eng:b'] because the arrow inherits team from listFixed’s scope.
Common gotchas
- Passing a method as a callback drops the receiver —
setTimeout(obj.m),arr.forEach(obj.m), ReactonClick={obj.m}all losethis. - Using an arrow as an object method or prototype method — it captures the wrong
thisand can’t be rebound. newon abind-ed function ignores the boundthis.- Assuming
bindis free — it creates a new function each call; in a render path, define it once. - Forgetting strict mode changes default
thisfromglobalThistoundefined. call/applyon an arrow silently has no effect on itsthis.
Interview questions & model answers
Q: How is this determined?
By the call site, via four rules in precedence order: new > explicit (call/apply/bind) > implicit (obj.method()) > default (undefined in strict, global in sloppy). Arrows are exempt — lexical this.
Q: Difference between call, apply, bind?
call and apply invoke immediately (call takes comma args, apply takes an array); bind returns a new function with this (and optional leading args) fixed for later.
Q: Why don’t arrow functions have their own this?
By design they capture this lexically from the enclosing scope, which makes them ideal for callbacks that need the surrounding context and immune to call/apply/bind.
Q: How do you fix a lost-this callback?
Bind it (fn.bind(obj)), wrap it in an arrow (() => obj.fn()), or use an arrow class field.
Q: What is this at the top level / in a plain call?
In strict mode / modules: undefined. In sloppy mode: the global object (window/globalThis).
Q: Can you change an arrow’s this with bind?
No. bind returns a new function but the arrow still uses its lexical this; the bound value is ignored.
Say it out loud (30s)
“
thisis bound at the call site by four rules:new, explicitcall/apply/bind, implicitobj.method(), then default (undefinedin strict mode). Arrow functions are the exception — they have no ownthis, they capture it lexically from where they’re defined, which is why they’re ideal for callbacks that need the surrounding context.”