In JavaScript, you're able to use a class
as a template for your objects:
class Car {
wheels = 4;
honk() {
console.log("Beep beep!");
}
}
// `fordCar` is an "instance" of Car
const fordCar = new Car();
console.log(fordCar.wheels); // 4
fordCar.honk();
As shown above, a class can have a collection of properties and methods. In addition to stateless methods, you can also reference the class instance and store state within the class object itself:
class Car {
// Gallons
gasTank = 12;
// Default MPG to 30
constructor(mpg = 30) {
this.mpg = mpg;
}
drive(miles = 1) {
// Subtract from gas tank
this.gasTank -= miles / this.mpg;
}
}
const fordCar = new Car(20);
console.log(fordCar.gasTank); // 12
fordCar.drive(30);
console.log(fordCar.gasTank); // 10.5
The this
keyword here allows us to mutate the class' instance and store values. However, the usage of this
can be dangerous and introduce bugs in unexpected ways, depending on context.
Let's take a look at:
- When
this
doesn't work as expected - How we can fix
this
withbind
- How to solve issues with
this
without usingbind
When does this
not work as expected?
Take the following two classes:
class Cup {
contents = "water";
consume() {
console.log("You drink the ", this.contents, ". Hydrating!");
}
}
class Bowl {
contents = "chili";
consume() {
console.log("You eat the ", this.contents, ". Spicy!");
}
}
cup = new Cup();
bowl = new Bowl();
If we run:
cup.consume();
It will console.log
"You drink the water. Hydrating!". Meanwhile, if you run:
bowl.consume();
It will console.log
"You eat the chili. Spicy!".
Makes sense, right?
Now, what do you think will happened if I do the following?
cup = new Cup();
bowl = new Bowl();
cup.consume = bowl.consume;
cup.consume();
While you might think that it would log "You eat the chili. Spicy!"
, it doesn't! Instead, it logs: "You drink eat the water. Spicy!"
.
Why?
The this
keyword isn't bound to the Bowl
class, like you might otherwise expect. Instead, the this
keyword searches for the scope of the caller.
To explain this better using plain English, this might be reiterated as: "JavaScript looks at the class that uses the
this
keyword, not the class that creates thethis
keyword"
Because of this:
cup = new Cup();
bowl = new Bowl();
// This is assigning the `bowl.consume` message
cup.consume = bowl.consume;
// But using the `cup.contents` `this` scoping
cup.consume();
Fix this
usage with bind
If we want bowl.consume
to always reference the this
scope of bowl
, then we can use .bind
to force bowl.consume
to use the same this
method.
cup = new Cup();
bowl = new Bowl();
// This is assigning the `bowl.consume` message and binding the `this` context to `bowl`
cup.consume = bowl.consume.bind(bowl);
// Because of this, we will now see the output "You eat the chili. Spicy!" again
cup.consume();
While bind
's functionality follows its namesake, it's not the only way to set the this
value on a method. You're also able to use call
to simultaneously call a function and bind the this
value for a single call:
cup = new Cup();
bowl = new Bowl();
cup.consume = bowl.consume;
// "You drink eat the water. Spicy!"
cup.consume();
// "You eat the chili. Spicy!"
cup.consume.call(bowl);
JavaScript's .call
method works like the following:
call(thisArg, ...args)
Such that you're not only able to call
a function with the this
value, but also pass through the arguments of the function as well:
fn.call(thisArg, arg1, arg2, arg3)
Can we solve this without .bind
?
The
.bind
code looks obtuse and increases the amount of boilerplate in our code. Is there any other way to solve thethis
issue withoutbind
?
Yes! Introducing: Arrow functions.
When learning JavaScript, you may have come across an alternative way of creating functions. Sure, there's the original function
keyword:
function SayHi() {
console.log("Hi");
}
But if you wanted to remove a few characters, you could alternatively use an "arrow function" syntax instead:
const SayHi = () => {
console.log("Hi");
}
Some people even start explanations by saying that there are no differences between these two methods, but that's not quite right.
Take our Cup
and Bowl
example from earlier:
class Cup {
contents = "water";
consume() {
console.log("You drink the ", this.contents, ". Hydrating!");
}
}
class Bowl {
contents = "chili";
consume() {
console.log("You eat the ", this.contents, ". Spicy!");
}
}
cup = new Cup();
bowl = new Bowl();
cup.consume = bowl.consume;
cup.consume();
We already know that this example will log "You eat the water. Spicy!"
when cup.consume()
is called.
But what happens if we instead change Bowl.consume()
from a class method to an arrow function:
class Cup {
contents = "water";
consume = () => {
console.log("You drink the ", this.contents, ". Hydrating!");
}
}
class Bowl {
contents = "chili";
consume = () => {
console.log("You eat the ", this.contents, ". Spicy!");
}
}
cup = new Cup();
bowl = new Bowl();
cup.consume = bowl.consume;
// What will this output?
cup.consume();
While it might seem obvious what the output would be, if you thought it was the same "You eat the water. Spicy!"
as before, you're in for a suprise.
Instead, it outputs: "You eat the chili. Spicy!"
, as if it were bound to bowl
.
Why does an arrow function act like it's bound?
That's the semantic meaning of an arrow function! While function
(and methods) both implicitly bind this
to a callee of the function, an arrow function is bound to the original this
scope and cannot be modified.
Even if we try to use .bind
on an arrow function to overwrite this behavior, it will never change its scope away from bowl
.
cup = new Cup();
bowl = new Bowl();
// The `bind` does not work on arrow functions
cup.consume = bowl.consume.bind(cup);
// This will still output as if we ran `bowl.consume()`.
cup.consume();
Problems with this
usage in event listeners
Let's build out a basic counter button that shows a button with a number inside. When the user clicks the button, it should increment the number inside of the button's text:
// This code doesn't work, we'll explore why soon
class MainButtonElement {
count = 0;
constructor(parent) {
this.el = document.createElement('button');
this.updateText();
this.addCountListeners();
parent.append(this.el);
}
updateText() {
this.el.innerText = `Add: ${this.count}`
}
add() {
this.count++;
this.updateText();
}
addCountListeners() {
this.el.addEventListener('click', this.add);
}
destroy() {
this.el.remove();
this.el.removeEventListener('click', this.add);
}
}
Let's see if this button works by attaching it to the document's <body>
tag:
new MainButtonElement(document.body);
It renders!
However, if we try to click the button, we get the following error:
Uncaught TypeError: this.updateText is not a function
Why is this?
We might get a hint if we add a console.log(this)
inside of our add()
method:
add() {
console.log(this);
// ...
}
<button>Add: 0</button>
It seems like this
is being bound to the button
HTMLElement
instance! 😱
How did this happen?
Well, remember that this
is being bound to something. In this case, it's being bound through the addEventListener
to the instance of the element in JavaScript.
We can then think of your browser calling an event on button
to look something like this:
/**
* This is a representation of what your browser is doing when you click the button.
* This is NOT how it really works, just an explainatory representation
*/
class HTMLElement {
constructor(elementType) {
this.type = elementType;
}
addEventListener(name, fn) {
for (let event of this.events) {
fn(event)
}
}
}
document.createElement("button");
Let's chart out what's happening behind-the-scenes:
Fixing this
event listener usage
To fix the issues with this
usage in event listeners, we can reuse our existing knowledge from earlier and do one of two things:
1) .bind
the usage of .add
in the event listener:
// This code doesn't work either
class MainButtonElement {
count = 0;
constructor(parent) {
this.el = document.createElement('button');
this.updateText();
this.addCountListeners();
parent.append(this.el);
}
updateText() {
this.el.innerText = `Add: ${this.count}`
}
add() {
this.count++;
this.updateText();
}
addCountListeners() {
this.el.addEventListener('click', this.add.bind(this));
}
destroy() {
this.el.remove();
// This won't remove the listener properly
this.el.removeEventListener('click', this.add.bind(this));
}
}
However, this has some problems, as two .bind
functions are not referentially stable:
function test() {}
console.log(test.bind(this) === test.bind(this)); // False
Which is required for removeEventListener
usage to remove the event listener properly. This means that we instead have to bind add
at the function's base:
class MainButtonElement {
count = 0;
constructor(parent) {
this.el = document.createElement('button');
this.updateText();
this.addCountListeners();
parent.append(this.el);
}
updateText() {
this.el.innerText = `Add: ${this.count}`
}
// 😖
add = (function() {
this.count++;
this.updateText();
}).bind(this)
addCountListeners() {
this.el.addEventListener('click', this.add);
}
destroy() {
this.el.remove();
this.el.removeEventListener('click', this.add);
}
}
Alternatively, we can...
- Use an arrow function rather than a class method:
class MainButtonElement {
count = 0;
constructor(parent) {
this.el = document.createElement('button');
this.updateText();
this.addCountListeners();
parent.append(this.el);
}
updateText() {
this.el.innerText = `Add: ${this.count}`
}
add = () => {
this.count++;
this.updateText();
}
addCountListeners() {
this.el.addEventListener('click', this.add);
}
destroy() {
this.el.remove();
this.el.removeEventListener('click', this.add);
}
}
Wrapping it up
Using the this
keyword is nearly unavoidable when using class-based JavaScript. It enables you to mutate state within the class to reference for later usage.
While some JavaScript is able to avoid this, it's particularly helpful to know when using frameworks such as Angular which uses classes as the primary means for defining a component.
Speaking of - want to learn how to use Angular? I'm writing a free book series called "The Framework Field Guide" that teaches React, Angular, and Vue all at once. Click the link to learn more about the book and be notified when it launches!
Happy hacking!
Top comments (1)
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍