codejump
Academy

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 object

2. 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 rebound

3. A method call — the object before the dot

obj.method();           // this === obj
arr[0]();               // this === arr

The 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 mode

The bug this produces, every time

const counter = {
  count: 0,
  increment() { this.count++; },
};

const inc = counter.increment;
inc();   // TypeError: Cannot read properties of undefined

inc 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);                     // broken

Three 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 this argument: look only at the call site. Is there a new? 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.