Dude, where's my this?

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);
    }
}

yours,
0x7f