this and Classes
"Put this in the fridge." Which fridge depends entirely on whose kitchen you're standing in.
You met this briefly in Objects, inside a method: this.openHour meant "this bakery's opening hour." It seemed simple. Then one day you'll pass that method to a button or a timer, and suddenly this is something else entirely, and your code breaks in a way that makes no sense.
this has a reputation as one of JavaScript's most confusing features. It doesn't have to be. There's one core rule, and a handful of situations to know.
The core rule: it depends on how you call it
For a regular function, this is decided at the moment the function is called, by how it's called. Not by where it was written.
Think of the word "here." If you text a friend "I'm here," the meaning depends entirely on where you are when you send it. The word doesn't change. The situation does.
1. Called as a method: this is the object before the dot
const bakery = {
title: "Maria's Bakery",
greet() {
console.log(`Welcome to ${this.title}!`);
},
};
bakery.greet(); // "Welcome to Maria's Bakery!"When you call bakery.greet(), look at what's before the dot: bakery. That's this. This is the case you'll use most, and it's the easy one.
2. Called on its own: this goes missing
Now pull the method out and call it by itself:
const greet = bakery.greet;
greet(); // "Welcome to undefined!"There's no dot, so there's no object to be this. In modern code (modules and classes, which use JavaScript's stricter "strict mode"), this is undefined and reading this.title throws an error. In an old-style plain script, like this book's editors, this falls back to the global window object, which has no title property. Either way, it's lost.
This happens by accident all the time, most often when handing a method to something else as a callback:
setTimeout(bakery.greet, 1000); // "Welcome to undefined!"You're passing the function itself, detached from bakery. When the timer calls it a second later, it's called on its own, with no dot.
3. Arrow functions don't have their own this
Arrow functions work differently. They don't get their own this at all. They use the this of the place where they were written, like any other outside variable (you learned about that reaching-outward behavior in Scope and Closures).
That makes arrows the easiest fix for the lost-this problem:
setTimeout(() => bakery.greet(), 1000); // "Welcome to Maria's Bakery!"The arrow calls bakery.greet() with the dot, at the right time. Problem solved.
But it also means arrows make poor methods:
const shop = {
title: 'Rye & Co',
greet: () => {
console.log(`Welcome to ${this.title}!`); // this is NOT shop here
},
};That arrow's this comes from outside the object, not from shop. So the rule from Arrow Functions and Callbacks stands: use the method shorthand (greet() {}) for methods, and arrows for callbacks.
4. In an event listener
With a regular function as a listener, this is the element the listener is attached to:
const button = document.querySelector('#favorite');
button.addEventListener('click', function () {
this.classList.toggle('is-active'); // this is the button
});With an arrow, it isn't. Modern code usually skips this here entirely and uses event.currentTarget instead, which always means "the element this listener is on," whichever kind of function you use:
const button = document.querySelector('#favorite');
button.addEventListener('click', (event) => {
event.currentTarget.classList.toggle('is-active');
});Clearer, and no guessing.
One more fix: bind
You'll see this in older code:
const boundGreet = bakery.greet.bind(bakery);
setTimeout(boundGreet, 1000); // worksbind makes a copy of the function with this permanently glued to the object you give it. It works, but an arrow wrapper usually reads more clearly.
Classes: a blueprint for objects
So far you've written every object by hand. But what if you need fifty products, or a hundred orders, all with the same shape and the same methods? You'd want a blueprint.
Think of a cookie cutter again, like <template> in Creating and Removing Elements, but for objects. A class describes what every object of that kind has and can do:
class Product {
constructor(name, price, stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
isAvailable() {
return this.stock > 0;
}
sell(quantity) {
this.stock -= quantity;
}
}And new stamps out an object from the blueprint:
const sourdough = new Product('Sourdough', 8, 12);
const rye = new Product('Rye', 7, 0);
console.log(sourdough.isAvailable()); // true
console.log(rye.isAvailable()); // false
sourdough.sell(3);
console.log(sourdough.stock); // 9class Productnames the blueprint. Class names start with a capital letter, by convention.constructorruns automatically when you callnew. Inside it,thisis the brand-new object being built, sothis.name = namesets up its properties.- Methods like
isAvailableare shared by every object made from the class. Inside them,thisis whichever object the method was called on, the object before the dot, just like case 1.
Each object made with new is called an instance. sourdough and rye are two separate instances, with their own stock counts.
Private fields and inheritance, briefly
Two more class features you'll meet in real code:
class Order {
#items = []; // private: only code inside the class can touch it
add(item) {
this.#items.push(item);
}
get count() {
return this.#items.length;
}
}
class GiftOrder extends Order {
constructor(message) {
super(); // run Order's own setup first
this.message = message;
}
}- A
#in front of a field makes it private, like the closure-protectedcountfrom Scope and Closures, but built into the class. get count()is a getter: you read it like a property,order.count, with no parentheses.extendsmakes a class that starts with everything another class has, andsuper()runs the parent's constructor.
You don't need classes for everything. Plain objects and functions handle most jobs in this book. Classes shine when you have many objects of the same kind, each carrying its own data and behavior.
Try it
HTML
JavaScript
Preview
Console
Nothing logged yet. Anything you pass to console.log() shows up here.
Try it yourself
- Press the button and watch the two greetings arrive. Which one lost its
this, and why? - Add a
sell(quantity)method toProductthat lowers the stock, sell 4 Focaccia before building the list, and see its label change. - Change
greet() {in thebakeryobject togreet: () => {and run it. What happens to the first greeting?
Check your understanding
Up next
You've now seen quite a few error messages. Time to stop fearing them and start reading them like clues, with tools to catch errors and track down bugs. That's Errors and Debugging.
