DEV Community

Cover image for Mastering JavaScript's `this` keyword using `bind`
Corbin Crutchley for This is Learning

Posted on • Originally published at unicorn-utterances.com

Mastering JavaScript's `this` keyword using `bind`

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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();
Enter fullscreen mode Exit fullscreen mode

If we run:

cup.consume();
Enter fullscreen mode Exit fullscreen mode

It will console.log "You drink the water. Hydrating!". Meanwhile, if you run:

bowl.consume();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 the this 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();
Enter fullscreen mode Exit fullscreen mode

Imagine bowl and cup as two boxes. Inside of the boxes are 2 items each. The

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();
Enter fullscreen mode Exit fullscreen mode

When using the

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);
Enter fullscreen mode Exit fullscreen mode

JavaScript's .call method works like the following:

call(thisArg, ...args)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 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:

function SayHi() {
    console.log("Hi");
}
Enter fullscreen mode Exit fullscreen mode

But if you wanted to remove a few characters, you could alternatively use an "arrow function" syntax instead:

const SayHi = () => {
    console.log("Hi");
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's see if this button works by attaching it to the document's <body> tag:

new MainButtonElement(document.body);
Enter fullscreen mode Exit fullscreen mode

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);
    // ...
}
Enter fullscreen mode Exit fullscreen mode

<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");
Enter fullscreen mode Exit fullscreen mode

Let's chart out what's happening behind-the-scenes:

When onClick is assigned to addOne, it doesn't carry over the  raw `this` endraw , because it isn't bound. As a result, when button.onClick is called, it will utilize Button's  raw `this` endraw  value.

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

However, this has some problems, as two .bind functions are not referentially stable:

function test() {}

console.log(test.bind(this) === test.bind(this)); // False
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can...

  1. 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍