frontend / javascript core / this-and-binding.md

this and Binding

4 min read source

this and Binding

TL;DR

this is not lexical for normal functions — it’s set by how the function is called, not where it’s defined. Four call patterns decide it: method call (obj.fn()obj), plain call (fn()undefined in strict mode / globalThis otherwise), new (a fresh object), and explicit call/apply/bind. Arrow functions are the exception: they have no own this and capture it lexically from the enclosing scope. The classic bug is method extraction — pulling a method off its object loses the binding.

Interview Q&A

Q: What determines this in a normal function?

A: The call site. Same function, four bindings:

function show() { return this; }
const obj = { show };

obj.show();          // obj            (method call)
show();              // undefined      (plain call, strict mode)
new show();          // {}             (construction — new object)
show.call("x");      // "x"            (explicit)

Resolution priority: new > explicit (call/apply/bind) > method > plain.

Q: How do arrow functions differ?

A: Arrows have no own this, arguments, super, or new.target. They close over this from the surrounding lexical scope at definition time, and it can’t be rebound:

const obj = {
  id: 1,
  regular() { return [1].map(function () { return this.id; }); }, // [undefined] — inner `this` is not obj
  arrow()   { return [1].map(() => this.id); },                   // [1] — arrow captures obj
};

This is exactly why arrows are the fix for “lost this” inside callbacks.

Q: What’s the method-extraction pitfall?

A: Assigning a method to a variable (or passing it as a callback) detaches it from its receiver:

const counter = {
  count: 0,
  inc() { this.count++; },
};
const f = counter.inc;
f();                 // TypeError: Cannot read properties of undefined (reading 'count')
setTimeout(counter.inc, 0);   // same problem — called as a plain function

Fixes: setTimeout(() => counter.inc(), 0), setTimeout(counter.inc.bind(counter), 0), or define inc as a class field arrow.

Q: call vs apply vs bind?

A:

Invokes now? Args
fn.call(thisArg, a, b) yes listed individually
fn.apply(thisArg, [a, b]) yes as an array
fn.bind(thisArg, a) no — returns a new function optionally pre-fills (partial application)

bind permanently fixes this (and any bound args) — a bound function cannot be re-bound, and new on a bound function ignores the bound this but keeps bound args.

Q: Class fields as arrows vs prototype methods — trade-off?

A:

class Btn {
  onClick = () => { this.handle(); };  // arrow field: bound per instance, survives extraction
  handle() {}                          // prototype method: shared, but `this` depends on call site
}

Arrow fields auto-bind (great for React event handlers, no bind in constructor) but create one function per instance (memory) and live on the instance, not the prototype — so they’re harder to spy/override in tests. Prototype methods are shared and overridable but need binding when passed as callbacks.

Q: What is this at module top level and in plain functions under strict mode?

A: In an ES module, top-level this is undefined. In a CommonJS module it’s module.exports. Inside a plain (non-method) function call, this is undefined under "use strict" (and all ES-module/class code is implicitly strict), or globalThis in sloppy mode.

Gotchas / edge cases

  • this in a standalone callback is not the objectarr.forEach(obj.method) calls method with this === undefined. Use arr.forEach(obj.method, obj) (forEach takes a thisArg) or wrap in an arrow.
  • bind then bind again does nothing — the first binding wins; the second is ignored.
  • Arrow as a method loses the object{ id: 1, get() { } } works, but { id: 1, get: () => this.id } captures module/global this, not the object.
  • new-ing an arrow throws — arrows are not constructors.
  • Event handlers: a normal function handler has this === the element; an arrow handler has the lexical this. React passes the event, so it rarely matters there, but it does with raw DOM addEventListener.
  • apply with a huge array can hit argument-count limits — use spread (fn(...bigArray)) only for moderate sizes; for max/min over large arrays, reduce instead.

What a senior is expected to say

  • this is dynamic for normal functions — decided by the call site, with priority new > bind/call > method > plain. Arrows capture this lexically and can’t be rebound.”
  • “Method extraction loses the receiver; I fix it with an arrow wrapper, bind, or a class arrow field.”
  • “Class arrow fields auto-bind but cost one function per instance and skip the prototype — fine for handlers, not for hot, shared methods.”
  • “All class and module code is strict mode, so a plain call gives this === undefined, not the global object.”

Cross-references

Further reading