Did you ever lose this in JavaScript?
A few days ago I've asked my team a trick question -- what will be logged in this code example:
class User {
constructor(fullName) {
this.fullName = fullName;
}
print() {
console.log(this.fullName);
}
}
class Test {
constructor() {
this.fullName = 'nope';
}
run(cb) {
cb();
}
}
const t = new Test();
const u = new User('0x7f');
t.run(u.print);
Devs who still don't hate JavaScript will respond with 0x7f
. But that's not true. In fact - this will result in an exception. this
is undefined. Let's see why.
Ever since class
was introduced to JavaScirpt it's been nothing more than just a syntax sugar for functions. Observe this simple example:
function User(fullName) {
this.fullName = fullName;
this.print = function () {
console.log(this.fullName);
};
}
This is the same piece of code like the User
from the above example. And it sheds a bit more light on why exactly our code does not work. Take the following usage:
const u = new User("0x7f");
const print = u.print;
print();
Now if we run this in a browser console you'll get undefined
. Why? Because it's using window
as parent scope, and window
does not have a fullName
property.
The main reason why we even get window
here (in browser), or nothing in a module (webpack, ts, w/e) is because of the way JavaScript handles references. When we passed a function reference u.print
we de facto lost the information about the scope we wanted. And when we don't have a scope defined we look for a parent one.
If you still don't quite get the reason why first example doesn't work - take a look at this:
const t = new Test();
const u = new User("0x7f");
const print = u.print;
t.run(print);
How do I fix this then?
There are two ways of fixing this to do the intended. First one is well known to React (pre-hook) devs:
class User {
constructor(fullName) {
this.fullName = fullName;
this.print = this.print.bind(this);
}
print() {
console.log(this.fullName);
}
}
This will make the u.print
forever bound to the User
scope.
The other solution, one that I do not like -- is using arrow functions instead of bind
:
class User {
constructor(fullName) {
this.fullName = fullName;
this.print = () => console.log(this.fullName);
}
}