Work out what this refers to in any situation, and fix the classic bug where a method loses its context.

Bishal Biswas
Self Learner
The one rule
this is not decided by where a function is written. It is decided by how the function is called.
Learn that sentence and most of the confusion disappears.
The four cases
1. Method call: this is the object before the dot
const person = {
name: 'Asha',
greet() {
return `Hi, I am ${this.name}`;
}
};
console.log(person.greet());
Called as person.greet(), so this is person.
2. Plain function call: this is undefined or the global object
function show() {
console.log(this);
}
show();
In a module or in strict mode, this is undefined. In a plain script it is window. Either way, it is not useful.
3. Constructor call: this is the new object
function User(name) {
this.name = name;
}
const asha = new User('Asha');
console.log(asha.name);
4. Explicit call: you choose
function greet() {
return `Hi, I am ${this.name}`;
}
const user = { name: 'Ben' };
console.log(greet.call(user));
console.log(greet.apply(user));
const bound = greet.bind(user);
console.log(bound());
The classic bug
const timer = {
seconds: 0,
start() {
setInterval(function () {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
};
timer.start();
This prints NaN forever. The inner function is called by setInterval, not on timer, so this is not timer.
Same thing with event handlers:
const counter = {
count: 0,
handleClick() {
this.count++;
console.log(this.count);
}
};
button.addEventListener('click', counter.handleClick);
counter.handleClick is passed as a plain function. When the click happens it is called without the dot, so this becomes the button.
Three ways to fix it
Arrow function
An arrow has no this of its own, so it uses the surrounding one.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
};
timer.start();
This is the modern default.
bind
button.addEventListener('click', counter.handleClick.bind(counter));
bind returns a new function permanently locked to that this.
Wrap in an arrow
button.addEventListener('click', () => counter.handleClick());
The call still has the dot, so this is correct.
call, apply and bind
All three set this. The difference is small.
function introduce(city, job) {
return `${this.name} from ${city} works as a ${job}`;
}
const person = { name: 'Ravi' };
console.log(introduce.call(person, 'Pune', 'developer'));
console.log(introduce.apply(person, ['Pune', 'developer']));
const bound = introduce.bind(person, 'Pune');
console.log(bound('designer'));
calltakes arguments one by one.applytakes them in an array.binddoes not call, it returns a new function you call later.
Memory tip:
callis for commas,applyis for an array,bindbinds for later.
In classes
Class methods have the same behaviour.
class Counter {
count = 0;
increment() {
this.count++;
return this.count;
}
incrementArrow = () => {
this.count++;
return this.count;
};
}
const c = new Counter();
const loose = c.increment;
const safe = c.incrementArrow;
console.log(safe());
loose() throws, because the method lost its object. safe() works, because an arrow class field captures this at construction time. This is why you see arrow fields in React class components.
this in event handlers
button.addEventListener('click', function () {
console.log(this);
});
button.addEventListener('click', function () {
console.log(this === event.currentTarget);
});
button.addEventListener('click', (event) => {
console.log(event.currentTarget);
});
With a regular function, this is the element the listener is attached to. With an arrow, use event.currentTarget instead.
Working it out
Ask these in order:
- Was it called with
new? Thenthisis the new object. - Was it called with
call,applyorbind? Thenthisis what you passed. - Was it called as
something.method()? Thenthisissomething. - Is it an arrow function? Then
thiscomes from the surrounding code. - Otherwise
thisisundefinedin modules and strict mode.
Quick recap
thisdepends on how a function is called, not where it is written.- Method call means the object before the dot.
- Passing a method as a callback loses that object.
- Arrow functions take
thisfrom the surrounding scope, which fixes callbacks. callandapplyinvoke immediately,bindreturns a locked function.- Arrow class fields keep
thiseven when the method is detached.
// 1. Method call: `this` is the object before the dot
const person = {
name: 'Asha',
greet() { return 'Hi, I am ' + this.name; }
};
console.log(person.greet());
// 2. Detached, so the object is lost
const detached = person.greet;
// console.log(detached()); // this.name is undefined
// 3. bind locks `this` permanently
const bound = person.greet.bind(person);
console.log(bound());
// call passes arguments with commas, apply passes an array
function introduce(city, job) {
return this.name + ' from ' + city + ' works as a ' + job;
}
const ravi = { name: 'Ravi' };
console.log(introduce.call(ravi, 'Pune', 'developer'));
console.log(introduce.apply(ravi, ['Pune', 'developer']));
// The classic callback bug, and the arrow fix
const timer = {
seconds: 0,
startBroken() {
setInterval(function () { this.seconds++; }, 1000); // wrong `this`
},
start() {
const id = setInterval(() => { // arrow keeps `this`
this.seconds++;
console.log('Tick', this.seconds);
if (this.seconds >= 3) clearInterval(id);
}, 1000);
}
};
timer.start();
// In classes, an arrow field survives being detached
class Counter {
count = 0;
increment() { this.count++; return this.count; } // needs the dot
incrementSafe = () => { this.count++; return this.count; };
}
const c = new Counter();
const safe = c.incrementSafe;
console.log(safe(), safe()); // 1 2, works even detached
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready