codejump

AcademyJavaScript

How this is decided at call time

The four rules that fix the value of this at call time, why extracting a method breaks it, and what arrow functions actually change.

3 min readUpdated

3 exercises

Waiting at the end of this page.

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

JavaScript
function User(name) { this.name = name; }
const u = new User('Ada');   // this === the new object

2. An explicit call, apply or bind

JavaScript
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

JavaScript
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

JavaScript
plain();                // undefined in modules and strict mode,
                        // globalThis in sloppy mode

The bug this produces, every time

JavaScript
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:

JavaScript
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:

JavaScript
// 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:

JavaScript
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

3 exercises

Reading this page is the cheap half. These are the exercises that make you use it.