Frontend / JavaScript core / this-and-binding.md

this and Binding

Updated 4 min read source
On this page6
  1. TL;DR
  2. In depth
  3. Gotchas / edge cases
  4. What a senior is expected to say
  5. Cross-references
  6. Further reading

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.

In depth

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 / 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 4

  • 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