this, and the four ways it gets set
Call-site rules, arrow functions, and the callback that loses its this.
After this lesson you can
- Predict what this refers to from how a function was called
- Explain why an arrow function has no this of its own
- Fix a method passed as a callback that has lost its this
this is not fixed to where a function is defined — it is set by
how the function is called, freshly, every time.
const user = {
name: "Nino",
greet() { return `Hi, ${this.name}`; },
};
user.greet(); // "Hi, Nino" — called as user.greet()
const g = user.greet;
g(); // "Hi, undefined" — called with no receiver
greet is the exact same function in both calls. What changed is the
call site: user.greet() sets this to user; g() sets this to
undefined (in strict mode, which modules are by default).
The four rules, in the order that matters
new Fn()—thisis the newly created object.fn.call(obj)/fn.apply(obj)/fn.bind(obj)—thisis whatever you explicitly hand it.obj.method()—thisisobj, the thing left of the dot.- A plain call,
fn()—thisisundefinedin strict mode (globalThisotherwise, which is almost never what anyone wants).
Try it
const counter = { count: 0, increment() { this.count += 1; return this.count; },}; export function run() { const results = []; results.push(counter.increment()); // called on counter const loose = counter.increment; try { loose(); // plain call, no receiver results.push("no error"); } catch (e) { results.push(e.constructor.name); } return results;}Arrow functions do not have their own this
An arrow function does not set this at all — it reads this from the
scope it was written in, the same way it reads any other outer
variable, and none of the four rules above apply to it.
const timer = {
label: "countdown",
start() {
setTimeout(function () {
console.log(this.label); // undefined — plain call, rule 4
}, 0);
setTimeout(() => {
console.log(this.label); // "countdown" — reads this from start()
}, 0);
},
};
This is the entire reason arrow functions exist for callbacks: a regular
function passed to setTimeout, an event listener, or .then() is
invoked as a plain call, rule 4, and loses whatever this the method had.
An arrow function passed the same way keeps it, because it never had a
this of its own to lose.
Fixing a method passed as a callback
class Button {
constructor(label) { this.label = label; }
onClick() { console.log(`clicked ${this.label}`); }
}
const b = new Button("Save");
el.addEventListener("click", b.onClick); // this is lost — rule 4
el.addEventListener("click", () => b.onClick()); // fixed — arrow reads b
el.addEventListener("click", b.onClick.bind(b)); // fixed — this is pinned
Both fixes work. .bind creates a new function permanently locked to
that this; the arrow wrapper is usually shorter and just as correct.
Try it yourself
2 visible tests · 2 hidden testsgreeter.greetAll(names) should return one greeting per name, each in
the form `${this.prefix}, ${name}`, using this.prefix — never
a hard-coded string. Implement greetAll so that mapping a callback
over names does not lose this the way a plain function callback
easily could.
run(["Nino","Ana"])run([])
Sign up to check the hidden tests and save your progress. Sign up