Did you ever lose this in JavaScript?

Posted on Saturday, 1 May 2021 by Andrei Z Crnkovic

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