Explain `this`, call, apply, and bind
`this` is determined at call time by how the function is invoked. `call`/`apply` invoke immediately with a chosen `this`; `bind` returns a new function permanently bound to it.
Of all the topics that trip up JavaScript candidates, this ranks #1 — and the reason is that the rules look ad-hoc until you internalize one core idea: this is resolved at the call site, not at the definition site (for regular functions). Arrow functions are the deliberate exception: they capture this lexically, the way variables do.
The four binding rules, in strict priority order:
newbinding.new Foo(...)creates a brand-new object, setsthisto it, runs the constructor body, and (unless the constructor returns its own object) returns the new object. Highest priority — overrides everything else.
- Explicit binding.
fn.call(ctx, a, b)invokesfnimmediately withthis === ctxanda, bas arguments.fn.apply(ctx, [a, b])is the same but takes arguments as an array — useful when you don't know the arity statically.fn.bind(ctx, a)returns a new function permanently bound toctxwithapartially applied.bindcannot be re-bound; the firstbindsticks. Withnew, the boundthisis ignored.
- Implicit binding. When called as a method (
obj.fn()),thisis the object the function was accessed from. Crucially, the binding lives at the call site, not the reference:const f = obj.fn; f()loses the binding because the method invocation form is gone. This is the source of countless "lostthis" bugs when handlers are detached and passed around.
- Default binding. No
new, no explicitcall/apply/bind, no method invocation →thisfalls back to the global object (window/globalThis) in non-strict mode, orundefinedin strict mode (and inside ES modules and class bodies, which are strict by default).
Arrow functions ignore all four rules. Arrows do not have their own this; lookup falls through to the enclosing function's this at the moment the arrow was defined. That's why obj.method = () => this is almost always a bug — it captures the outer scope's this (which, in a module, is undefined), not obj. Conversely, arrow callbacks are perfect for situations where you want this to inherit: array.map(x => this.transform(x)) inside a method does the right thing without bind.
Call vs apply vs bind — when to use which:
call: known argument count, spread inline.fn.call(ctx, a, b, c).apply: argument array. Common before ES6 spread for forwarding arguments:fn.apply(this, arguments). With ES6,fn.call(this, ...arguments)is equivalent.bind: you need a long-lived reference withthislocked in (event handlers, callbacks passed to libraries that don't preserve context). Also enables partial application:fn.bind(ctx, a)is a new function that always prependsa.
Concrete examples that interviewers ask about:
const user = { name: 'Ada', hi() { return \`Hi, \${this.name}\`; } };
user.hi(); // "Hi, Ada" — implicit binding
const f = user.hi; f(); // "Hi, undefined" (strict) — binding lost
user.hi.call({name:'Bob'}); // "Hi, Bob" — explicit binding wins
const bound = user.hi.bind({name:'Eve'});
bound(); // "Hi, Eve"
new (function() { console.log(this); })(); // {} new instance — new bindingClass methods. Methods on the prototype are not auto-bound. const fn = instance.method; fn() loses this. Two common fixes: bind in the constructor (this.method = this.method.bind(this)), or use arrow class fields (method = () => {...}). Both create per-instance allocations — fine for typical UI components, but worth knowing for tight allocation patterns.
this in callbacks is the classic React 15 pain: <button onClick={this.handleClick}> lost this, so you wrote onClick={this.handleClick.bind(this)} (creates a new function every render, breaks React.memo) or arrow class fields. With hooks and function components, the problem disappears — there's no this to lose.
Pitfalls and edge cases:
bindis one-shot.fn.bind(a).bind(b)is still bound toa— the secondbindis ignored forthis(it can still add partial args).bindwithnewignores the boundthisand creates a fresh instance.- Strict mode default — top-level
thisisundefinedin modules; legacy scripts getwindow. - Method extraction (
document.addEventListener('click', this.handler)) losesthisunless you bind or use arrow fields. - Arrow functions in object literals —
{ greet: () => this.name }captures the enclosingthis, not the object. - Each
bindallocates a new function — avoid in hot render paths.
The summary for interviews. "Regular functions resolve this at the call site by one of four rules in priority order: new, explicit (call/apply/bind), implicit (method call), default. Arrow functions ignore all four and inherit this lexically. call and apply invoke immediately; bind returns a new function permanently bound to a context."
Code
Follow-up questions
- •Why doesn't `this` work in an arrow function the way you might expect?
- •Implement Function.prototype.call from scratch.
- •How does class method binding interact with React event handlers?
Common mistakes
- •Detaching a method from its object (`const f = obj.method`) and expecting `this` to survive.
- •Using arrow functions as object methods or class prototype methods.
- •Calling `bind` repeatedly — only the first `bind` sticks; the second is ignored.
Performance considerations
- •Each `bind` allocates a new function — avoid in tight loops or hot render paths.
- •Class methods bound in the constructor allocate per-instance; arrow class fields allocate per-instance too. Prototype methods are shared — bind at the call site if you need stable identity.
Edge cases
- •`bind` cannot be re-bound — `fn.bind(a).bind(b)` is still bound to `a`.
- •Calling a bound function with `new` ignores the bound `this` and creates a new instance.
Real-world examples
- •React class components used to need `this.handler = this.handler.bind(this)` in the constructor before arrow class fields became common.
- •Array-likes use `Array.prototype.slice.call(arguments)` to convert to a real array (pre-ES6).