this and Binding
Date: 2026-09-28
thisis 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 takingthisfrom 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
newbeatsbind. Calling a bound function withnewignores the boundthisand uses the new object — so rule 1 genuinely outranks rule 2bindis permanent. Binding an already-bound function again does nothing tothis- DOM event handlers — a regular-function listener gets
this === event.currentTarget. An arrow listener doesn’t. Preferevent.currentTargetexplicitly 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—undefinedin a module,windowin a classic script — Modules - Class bodies are strict, so a detached method gets
undefined, notwindow— which is kinder: it throws immediately rather than writing to a global
The tradeoff in the fixes
| Fix | Cost |
|---|---|
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 constructor | Same 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 this | Functions 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.