mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 12:57:45 +00:00
docs: rename article, finish initial draft
This commit is contained in:
535
content/blog/javascript-bind-usage/index.md
Normal file
535
content/blog/javascript-bind-usage/index.md
Normal file
@@ -0,0 +1,535 @@
|
||||
---
|
||||
{
|
||||
title: "JavaScript `this` binding & Angular Usage",
|
||||
description: "",
|
||||
published: '2023-03-16T21:52:59.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['javascript', 'computer science'],
|
||||
attached: [],
|
||||
license: 'cc-by-4'
|
||||
}
|
||||
---
|
||||
|
||||
In JavaScript, you're able to use a `class` as a template for your objects:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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` with `bind`
|
||||
- How to solve issues with `this` without using `bind`
|
||||
|
||||
# When does `this` not work as expected?
|
||||
|
||||
Take the following two classes:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
cup.consume();
|
||||
```
|
||||
|
||||
It will `console.log` "You drink the water. Hydrating!". Meanwhile, if you run:
|
||||
|
||||
```javascript
|
||||
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?
|
||||
|
||||
```javascript
|
||||
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](https://developer.mozilla.org/en-US/docs/Glossary/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 the `this` keyword"
|
||||
|
||||
Because of this:
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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 components. Is there any other way to solve the `this` issue without `bind`?
|
||||
|
||||
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:
|
||||
|
||||
```javascript
|
||||
function SayHi() {
|
||||
console.log("Hi");
|
||||
}
|
||||
```
|
||||
|
||||
But if you wanted to remove a few characters, you could alternatively use an "arrow function" syntax instead:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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`.
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 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 the problem with arrow functions
|
||||
|
||||
To fix the issues with `this` usage in event listeners, we can do one of two things:
|
||||
|
||||
1) **`.bind` the usage of `.add` in the event listener:**
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
```javascript
|
||||
function test() {}
|
||||
|
||||
console.log(test.bind(this) === test.bind(this)); // False
|
||||
```
|
||||
|
||||
This means that we instead have to bind `add` at the function's base:
|
||||
|
||||
```javascript
|
||||
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...
|
||||
|
||||
2. **Use an arrow function rather than a class method**:
|
||||
|
||||
```javascript
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works because arrow functions behave differently from `function` keyword functions.
|
||||
|
||||
**Arrow functions do not allow `this` to be rebound, even with `bind` or `call` usage**. This means that when `this` is set to `MainButtonElement` in the class, it will never rebind again, even when called inside of `HTMLElement`'s `addEventListener` usage.
|
||||
|
||||
We can see this in action in the demo from before:
|
||||
|
||||
```javascript
|
||||
class Cup {
|
||||
contents = "water";
|
||||
|
||||
// Notice the arrow functions, `this` won't rebind
|
||||
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();
|
||||
```
|
||||
|
||||
Which will now output:
|
||||
|
||||
> You eat the chili. Spicy!
|
||||
Reference in New Issue
Block a user