It depends on the call, not the code
this is not decided where a function is written. It is decided by how it is called, and there are exactly five shapes. Learn the five and the mystery disappears.
obj.method() // this === obj — "what is to the left of the dot"
fn() // undefined (strict) — a plain call
new Fn() // the new object
fn.call(x, a, b) // x — or .apply(x, [a, b]), or .bind(x)
el.addEventListener("click", fn) // the element
// and an arrow function has none of its own: it uses the enclosing scope
The one that breaks
Passing obj.method anywhere detaches it. The value passed is just the function; nothing remembers which object it came from.
const timer = {
seconds: 0,
tick() { this.seconds += 1; },
};
timer.tick(); // fine — called with the dot
const t = timer.tick;
t(); // TypeError: this is undefined
setTimeout(timer.tick, 1000); // same problem: the dot is gone by the time it runs
Three fixes
// 1. an arrow keeps the surrounding this
setTimeout(() => timer.tick(), 1000);
// 2. bind returns a new function with this fixed
setTimeout(timer.tick.bind(timer), 1000);
// 3. in a class, bind in the constructor, or use a field
class Timer {
seconds = 0;
tick = () => { this.seconds += 1; }; // a field holding an arrow
}
Often the function does not need this. A plain function taking the state as an argument cannot lose it: tick(timer).
call, apply, bind
bind returns a new function; it does not change the original. Binding an already-bound function has no further effect — the first binding wins.
function describe(prefix) {
return `${prefix} ${this.name}`;
}
describe.call({ name: "Painted" }, "The"); // arguments listed
describe.apply({ name: "Painted" }, ["The"]); // arguments in an array
const bound = describe.bind({ name: "Painted" });
bound("The"); // a NEW function, bound forever
// bind can pre-fill arguments too
const the = describe.bind({ name: "Veneer" }, "The");
the(); // "The Veneer"
Arrow functions have no this
An arrow also has no arguments, no prototype, and cannot be used with new. Those are the same trade: it is a function without its own identity.
// RIGHT: inside a method, so the arrow inherits the method's this
const timer = {
seconds: 0,
start() {
setInterval(() => { this.seconds += 1; }, 1000);
},
};
// WRONG: as the method itself — this is the module, not the object
const broken = {
name: "Painted",
describe: () => `It is ${this.name}`, // undefined
};
In an event handler
// a function expression: this is the element
button.addEventListener("click", function () {
this.disabled = true; // works
});
// an arrow: this is whatever surrounds the code
button.addEventListener("click", () => {
this.disabled = true; // NOT the button
e.currentTarget.disabled = true; // use this instead
});
e.currentTarget is always the element the listener is attached to, whichever function form you used. Relying on it means the handler works either way.
In a class
A field costs one function per instance; a prototype method is shared. For a handful of components it does not matter; for thousands of instances it does.
class Counter {
n = 0;
inc() { this.n += 1; } // prototype method — loses this if detached
dec = () => { this.n -= 1; }; // field holding an arrow — bound to the instance
}
const c = new Counter();
const inc = c.inc;
inc(); // TypeError
const dec = c.dec;
dec(); // fine
globalThis
At the top level of a module, this is undefined. In a classic script it is the global object. globalThis is the portable name for that object — window in a browser, global in Node — and you should very rarely need it.