The problem with `this` in JS

July, 2021712 words

Both starters and experienced developers in JS have one thing in common. Sometimes this behaves in a way you don't expect and you don't know why. But what exactly is so special about this little keyword in Javascript?

First the good thing

In the simplest form this behaves exactly the way you'd expect it to work.

Let's take this class as an example.

class Output {
constructor() {
this.prop = "Hello world";
}
print() {
console.log(this.prop)
}
}

This class defines a method that prints its prop property to the console. To make it clear we're referencing the prop from this exact instance we have to use this.

If you call this method you'll receive the expected output

Hello world

You might be wondering now: So what's the problem? Works as expected!

Then take a look at the next example.

const output = new Output();
const printCallback = output.print;

We're creating an object of Output and store a reference of its print method inside a constant. If you call this callback method you get the expected output of...

Cannot read properties of undefined

Wow that's unexpected, isn't it? Why does that happen?

Well, it turns out that although the this keyword, same as in other languages, means we want to reference this object, in Javascript the value of this depends on where you call the function.

So when you call output.print() the this inside of the print method is set to the output object. That's why you will get the expected output.

If you create a callback to this method though, when you call it the value of this is, again, set to whatever calls the function. And because the callback was created in the global scope (in Deno we don't have a global this), the callback code can't find the required property on an undefined object and gives us an error.

But there must be a way how we can pass callbacks and still reference the same property as when we defined the method.

Arrow functions come to rescue

As you might know, in Javascript we have two options to create a function.

  • by function declaration: function name(...)
  • by arrow function: () => {...}

If you've worked with arrow functions before you might have already used one of their features without actually looking at it. When defining an arrow function, the value of this remembers whatever it was when the arrow function was created. This means that when passing it around, e.g. as a callback, no matter from where you call this function, you will always get the same result.

class Output {
constructor() {
this.prop = "Hello world";
this.print = () => console.log(this.prop);
}
}

This code looks almost identical to the one above, but pay attention to this detail this.prop = () => ... Because we use an arrow function instead of a regular function declaration, we get completely different behavior when it comes to the this keyword.

Storing and calling this method as a callback now will lead to the following:

Hello world

Yes 👍 This is exactly the behavior we wanted and expected.

When do you need this?

Well depending on what you do, you might encounter this problem quite frequently. I stumbled across this issue when I first started with React. Back then I build Class-Components and defined all my callback methods as functions of my component and passed them down via props to other components. But because I didn't fully understand the behavior of this I got errors whenever the callback got triggered.

One more solution

Let's say you just don't want to use an arrow function. What else can you do to prevent this issue from happening? Luckily, Javascript has a special function on its Prototype called bind. Whenever you'd want to keep the this value from where the function was defined, you'd simply call:

yourFunction.bind(objectToBindTo);

This function returns a completely new function reference, that will invoke the exact same code you defined in the original function. But now the value of this inside of the function is set to the object you pass to the bind method.

No more unexpected errors when you use this inside of the function, you can simply and safely pass this new function around without worrying about what or who the caller is.

Conclusion

No matter what path you choose, arrow functions, or binding the value, it's always worth noting spending time to understand what really happens in Javascript will save you time and a headache later.