How `this` is decided
The four rules that fix the value of this at call time, why extracting a method breaks it, and what arrow functions actually change.
Updated
this is not about where the function was written
For a normal function, this is decided at the moment of the call, by how it is called — not by where it was defined, not by which object it happens to be stored on. The same function can have a different this on every call.
Four rules, checked in order. The first that applies wins.
1. new — a fresh object
function User(name) { this.name = name; }
const u = new User('Ada'); // this === the new object2. An explicit call, apply or bind
greet.call(obj); // this === obj
greet.apply(obj, args); // same, arguments as an array
const bound = greet.bind(obj); // permanently, and it cannot be rebound3. A method call — the object before the dot
obj.method(); // this === obj
arr[0](); // this === arrThe key word is before the dot at the call site. Not "the object it belongs to".
4. Nothing — the default
plain(); // undefined in modules and strict mode,
// globalThis in sloppy modeThe bug this produces, every time
const counter = {
count: 0,
increment() { this.count++; },
};
const inc = counter.increment;
inc(); // TypeError: Cannot read properties of undefinedinc is the same function. What was lost is the call site — there is no object before the dot any more, so rule 4 applies and this is undefined.
This is why passing a method as a callback fails:
button.addEventListener('click', counter.increment); // broken
setTimeout(counter.increment, 100); // broken
[1, 2].forEach(counter.increment); // brokenThree fixes, in order of preference: counter.increment.bind(counter), an arrow wrapper () => counter.increment(), or a class field increment = () => { ... } which binds once per instance at construction.
What arrow functions actually do
An arrow function has no `this` of its own. It is not "bound to the enclosing scope" by some special mechanism — this inside it is simply not a thing it defines, so the identifier resolves outward like any other variable, to whatever this the enclosing function had.
That is exactly why the old workaround disappeared:
// before
const self = this;
items.forEach(function (i) { self.total += i; });
// after
items.forEach((i) => { this.total += i; });And exactly why an arrow is the wrong choice for a method on an object literal:
const obj = {
name: 'Ada',
greet: () => `Hi ${this.name}`, // this is NOT obj
};There is no enclosing function, so this is the module or global this — never obj. Rules 2 and 3 do not apply to arrows at all: arrow.call(obj) is ignored, silently.
Classes are strict, always
Class bodies run in strict mode whatever the surrounding file does, so a detached method gives you undefined rather than the global object. That is a mercy: a TypeError at the call is far easier to find than properties quietly written onto globalThis.
A quick way to settle any
thisargument: look only at the call site. Is there anew? An explicit.call/.bind? A dot? Nothing? The answer is in that one line — unless the function is an arrow, in which case the answer is wherever it was written.
Now practice it
Reading this page is the cheap half. These are the exercises that make you use it.
- Quizeasy
Object-Oriented Programming · Classes & Objects
Objects, methods and construction — where the first three rules show up in practice.
- Quizmedium
Object-Oriented Programming · Inheritance & Prototypes
Inheritance and prototypes, including what `this` does across a `super` call.
- Quizmedium
Output & Coercion Traps · Tricky Outputs
Predict-the-output snippets, which is the format every `this` question is really asked in.