Tags: web-dev concept

this and Binding

Date: 2026-09-28


this is set by how a function is called, not where it’s written. Four rules decide it, checked in a fixed order — and arrow functions opt out of all four by taking this from the scope around them.


this is an implicit parameter of every non-arrow function whose value is bound at call time by the form of the call; arrow functions have no this of their own and resolve it lexically, like any other variable.

The four rules, highest precedence first

how is it called?                         this =
────────────────────────────────────────────────────────────────
1  new Fn()                               the freshly created object
2  fn.call(x) · fn.apply(x) · fn.bind(x)  x
3  obj.fn()                               obj   ← the thing before the dot
4  fn()                                   undefined (strict / modules / classes)
                                          globalThis (sloppy scripts)

arrow function                            whatever `this` was where the arrow
                                          was DEFINED. call/apply/bind can't
                                          change it; `new` throws

Only the call expression counts. obj.fn() and const f = obj.fn; f() call the same function and get different this — the second is rule 4.

The load-bearing sample

class Basket {
  items = [];
  add(item) { this.items.push(item); }            // method on the prototype
  addBound = (item) => { this.items.push(item); } // arrow in a class field: per instance
}
 
const basket = new Basket();                      // rule 1: this = the new object
basket.add('shoe');                               // rule 3: this = basket ✓
 
const { add } = basket;
add('sock');            // rule 4: this = undefined → TypeError reading 'items'
 
button.addEventListener('click', basket.add);     // this = button (the DOM sets it) ✗
button.addEventListener('click', basket.addBound);// arrow: this fixed at construction ✓
button.addEventListener('click', () => basket.add('hat')); // rule 3 inside the arrow ✓

Passing a method loses its object. basket.add as a value is just the function; the dot that would have supplied this is gone by the time something else calls it. That’s the entire “lost this” bug.

Edge cases worth knowing once

  • new beats bind. Calling a bound function with new ignores the bound this and uses the new object — so rule 1 genuinely outranks rule 2
  • bind is permanent. Binding an already-bound function again does nothing to this
  • DOM event handlers — a regular-function listener gets this === event.currentTarget. An arrow listener doesn’t. Prefer event.currentTarget explicitly and the question disappears — Event Handling
  • Callbacks with a thisArg — array.map(fn, thisArg) sets rule 2 for you; mostly obsolete since arrows
  • Top-level this — undefined in a module, window in a classic script — Modules
  • Class bodies are strict, so a detached method gets undefined, not window — which is kinder: it throws immediately rather than writing to a global

The tradeoff in the fixes

FixCost
Arrow class field (addBound = () => …)One function per instance, not shared on the prototype. Can’t be overridden via super or stubbed on the prototype in tests — Prototypes and Inheritance
this.add = this.add.bind(this) in the constructorSame per-instance cost, more ceremony. The pre-class-fields idiom
Arrow at the call site (() => basket.add(x))A new function each time it runs — which matters when it’s a prop, because it breaks referential equality for memoised children
Not using thisFunctions taking explicit arguments and closures over state — why most React code stopped writing classes, and why the question rarely arises in it

vs PHP: $this is always the instance in a method, and a closure defined in a method captures $this automatically — PHP never had the detached-method problem because turning a method into a value ($basket->add(...), or [$basket, 'add']) keeps the object attached.