We've been bumping into this since Grade 8, and we promised we'd explain it properly in College. Here we are!
The tricky thing about this is that its value isn't decided when you write the function. It's decided when the function is called, based on how it's called. There are four rules.
Rule 1: Called as a method → this is the object before the dot
const dog = {
name: "Bingo",
bark() {
return `${this.name} says woof!`;
},
};
console.log(dog.bark());
It prints
Bingo says woof!
dog.bark() is called with dog before the dot, so inside bark, this is dog. This is by far the most common case.
Rule 2: Called with new → this is the brand-new object
const hero = new Character("Mia", 100);
Inside the constructor, this is the new object being built. You've seen this all through this chapter.
Rule 3: Called on its own → this is undefined
Here's where things go wrong. If you take a method out of its object and call it on its own, there's no object before the dot, so this is lost:
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
return this.count;
}
}
const counter = new Counter();
console.log(counter.increment());
const loose = counter.increment;
loose();
It prints
1 Uncaught TypeError: Cannot read properties of undefined (reading 'count')
loose() has nothing before the dot, so this is undefined, and undefined.count crashes. (In some older, non-class code, this falls back to the global object instead of undefined, which is even more confusing. Classes always use the stricter, safer undefined behavior.)
This "lost this" problem happens most often when you pass a method as a callback, like button.addEventListener("click", counter.increment) or setTimeout(counter.increment, 1000). The method gets called on its own later, without its object.
Rule 4: Arrow functions don't have their own this
Arrow functions don't play by the rules above. They don't get their own this at all. Instead, they use the this from the surrounding code where they were written. That's why they make bad object methods (Grade 8), but they're perfect for callbacks inside methods:
class Team {
constructor(name, members) {
this.name = name;
this.members = members;
}
introduce() {
this.members.forEach((member) => {
console.log(`${member} plays for ${this.name}`);
});
}
}
new Team("Red Rockets", ["Mia", "Omar"]).introduce();
It prints
Mia plays for Red Rockets Omar plays for Red Rockets
The arrow function inside forEach borrows this from introduce, where this is the team. If we'd used a regular function (member) { ... } callback there, it would have been called on its own (Rule 3), and this.name would crash.
Fixing a lost this: bind
If you need to pass a method as a callback, you have two easy fixes. Wrap it in an arrow function, or bind it. bind creates a copy of the function with this permanently locked to the object you choose:
const fixed1 = () => counter.increment();
const fixed2 = counter.increment.bind(counter);
console.log(fixed1());
console.log(fixed2());
It prints
2 3
obj.method():thisisobj(the thing before the dot).new Thing():thisis the new object.fn()on its own:thisisundefined(or the global object in old-style code).- Arrow functions: no
thisof their own. They use thethisof the code around them.
When in doubt, ask: "How is this function being called?" And when you pass a method as a callback, wrap it in an arrow function or use bind.