Frontend / JavaScript core / 04_this_and_binding.md

this and Binding

Updated 3 interview angles 4 min read source
On this page10
  1. What determines this in a normal function?
  2. How do arrow functions differ?
  3. What’s the method-extraction pitfall?
  4. call vs apply vs bind?
  5. Class fields as arrows vs prototype methods — trade-off?
  6. What is this at module top level and in plain functions under strict mode?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

this and Binding

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.

What determines this in a normal function?

The call site. Same function, four bindings:

js
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.

How do arrow functions differ?

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:

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

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

What’s the method-extraction pitfall?

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

js
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.

call vs apply vs bind?

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.

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

js
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.

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

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

  • this in a standalone callback is not the object — arr.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.

Further reading

Interview angle 3

  • “What determines this in a normal function?” - the call site, not where the function was written. The precedence is new, then an explicit bind/call/apply, then a method call on a receiver, then a plain call. Arrow functions have no this of their own - they close over the enclosing scope’s, and cannot be rebound at all.
  • “You pass obj.method as a callback and it breaks. Why?” - extracting the function separates it from its receiver, so at call time there is no object on the left of the dot. Wrap it in an arrow, bind it, or make it a class arrow field.
    • Follow-up: “Which of those would you choose?” - an arrow wrapper at the call site for a one-off. A class arrow field auto-binds, but it allocates a function per instance and lives off the prototype, which is fine for event handlers and wrong for a hot method shared by many instances.
  • “A plain function call gives undefined instead of window. Why?” - strict mode, which all class bodies and all modules are in by default. The old sloppy-mode behaviour of substituting the global object does not apply, and that is a better default because the failure is loud.