Lesson 18 of 28
The this Keyword
The value of this is decided by how a function is called, not where it is written — the four call patterns, arrow functions, and the lost-this callback bug.
You pass a method to setTimeout and it throws Cannot read properties of undefined. The method did not change. What changed is how it got called. That
sentence is nearly the whole lesson: this is decided at the call site.
this is a hidden parameter
Every normal function receives an extra argument you never declare. JavaScript
fills it in based on how the call was written, then makes it available as
this. Nothing about where the function was defined affects it.
So the useful question is never "what is this here?" It is "how was this
function called?" There are four answers.
Call pattern one: a plain call
function whoAmI() {
return this;
}
whoAmI(); // undefinedIn a module or in strict mode, a plain call sets this to undefined. In an
old non-strict script it becomes the global object instead, which is worse — it
hides the mistake and lets you write to globals by accident. All module code is
strict, so treat undefined as the rule.
Call pattern two: a method call
If there is a dot, this is whatever is to the left of it.
const cart = {
currency: "INR",
items: [{ price: 199 }, { price: 499 }],
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
},
};
cart.total(); // 698 — this is cartThe object does not own the function in any deep sense. It just happened to be
before the dot at call time. Move the same function somewhere else and this
moves with the new call site.
const other = { items: [{ price: 50 }] };
other.total = cart.total;
other.total(); // 50 — same function, different thisCall pattern three: new
new creates a fresh empty object, sets this to it, runs the function, and
returns that object.
function Lesson(title, minutes) {
this.title = title;
this.minutes = minutes;
}
const lesson = new Lesson("Closures", 13);
console.log(lesson.title); // "Closures"Forget the new and it degrades into pattern one: this is undefined and
the assignment throws. This is one reason class exists — calling a class
without new is an error rather than a silent mess.
Call pattern four: call, apply and bind
You can set this yourself.
function describe(prefix) {
return prefix + " " + this.title;
}
const lesson = { title: "Closures" };
describe.call(lesson, "Lesson:"); // "Lesson: Closures"
describe.apply(lesson, ["Lesson:"]); // same, arguments as an array
const describeLesson = describe.bind(lesson);
describeLesson("Lesson:"); // "Lesson: Closures"call and apply invoke immediately and differ only in how arguments are
passed. bind invokes nothing — it returns a new function with this locked
in. A bound function cannot be rebound, so bind once and keep the result.
Arrow functions have no this
An arrow does not get its own this. It uses the this of the code around it,
decided when it was written rather than when it is called. Nothing you do at
the call site changes that — not call, not bind.
That makes arrows correct for callbacks:
const cart = {
items: [{ price: 199 }, { price: 499 }],
currency: "INR",
labels() {
return this.items.map((item) => `${item.price} ${this.currency}`);
},
};
cart.labels(); // ["199 INR", "499 INR"]The arrow inside map sees the labels method's this, so this.currency
works. Write that callback as function (item) { ... } instead and this
becomes undefined inside it.
And it makes arrows wrong for methods:
const broken = {
currency: "INR",
label: () => this.currency, // this comes from outside the object
};
broken.label(); // undefined, or a TypeError in a moduleAn object literal does not create a scope, so the arrow reaches past it to
whatever this the surrounding file has. Use shorthand methods for methods and
arrows for callbacks.
A plain function nested inside a method
This one catches people who have already learned the rules, because the nested function looks like it is inside the method and therefore inside the object.
const cart = {
items: [{ price: 199 }, { price: 499 }],
currency: "INR",
report() {
this.items.forEach(function (item) {
console.log(item.price, this.currency); // TypeError
});
},
};forEach calls that callback plainly, so this is undefined inside it. The
method's this is irrelevant — the callback got its own.
Before arrows, people worked around it by copying this into a variable, and
you will still see this in older code:
report() {
const self = this;
this.items.forEach(function (item) {
console.log(item.price, self.currency);
});
}An arrow does the same job without the extra name. Most array methods also
accept a second thisArg argument — forEach(callback, this) — but it only
works for function callbacks and reads worse than an arrow. Use the arrow.
The bug you will actually hit
Passing a method somewhere else strips the dot, and the dot was the only thing
supplying this.
class Player {
constructor() {
this.playing = false;
}
toggle() {
this.playing = !this.playing;
}
}
const player = new Player();
button.addEventListener("click", player.toggle);
// TypeError: Cannot set properties of undefinedaddEventListener stores the function and later calls it plainly. There are
three fixes, and they are all common in real code.
// 1. bind at the point of use
button.addEventListener("click", player.toggle.bind(player));
// 2. wrap in an arrow, which keeps the dot
button.addEventListener("click", () => player.toggle());
// 3. define it as a class field, bound once per instance
class Player {
playing = false;
toggle = () => {
this.playing = !this.playing;
};
}The arrow wrapper is usually clearest. The class field is convenient but puts a separate function on every instance instead of one on the prototype, which matters only when you create very many objects.
Try it yourself
The first line matters. Without it the detached call gets the global object
instead of undefined, and the bug hides behind a NaN rather than throwing.
One extra case: event handlers
When the DOM calls a handler you registered with addEventListener, it sets
this to the element the listener is attached to.
button.addEventListener("click", function () {
console.log(this.textContent); // the button's text
});
button.addEventListener("click", () => {
console.log(this); // NOT the button — the arrow ignores the DOM
});event.currentTarget gives you the same element without relying on this, so
in modern code you can use arrows everywhere and read the element from the
event.
setTimeout behaves differently again. It calls your callback as a plain
function, but in a browser the timer sets this to the global object rather
than undefined, so a mistake there fails silently instead of throwing.
setTimeout(function () {
console.log(this); // Window in a browser, a Timeout object in Node
}, 0);The lesson is not to memorise each host's behaviour. It is to stop relying on
this in any callback and use an arrow, which simply keeps the this you
already had.
What to remember
thisdepends on the call, not the definition. Read the call site first.- Plain call:
undefined. Dot call: the object before the dot.new: the new object.call/apply/bind: whatever you passed. - Arrows have no
this— right for callbacks, wrong for object methods and constructors. - A method handed off as a callback loses
this; fix it withbind, an arrow wrapper, or a class field.
Check yourself
4 questions · pass 3/4 to unlock Objects, Classes and Prototypes
1.A module defines an object with an
inc()method that doesthis.count += 1. You doconst inc = counter.incand callinc(). What happens?2.What decides the value of
thisinside a normal function?3.Where is an arrow function the right choice?
4.What does
handleClick.bind(component)do?
4 left to answer