July 14, 2016

Understanding ‘this’ in JavaScript

Filed under: Technical — Tags: , , — James Bunton @ 10:27 pm

I believe the behaviour of the this keyword in JavaScript is one of the most confusing and poorly thought out parts of the language. If you understand how objects and classes work in some other language, but don’t understand why JavaScript behaves weirdly, this blog aims to help you out. By the end you’ll understand how this works and how to make it work for you.

I’ll start out with a bit of background information on how classes work in JavaScript. If you already understand the ES6 class syntax then you can skip the first section.

I’ll then cover the most common ways that this can surprise you, why it behaves that strangely and finally how to make it do what you want.

Classes in JavaScript

class Person {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log('Hello, my name is ' + this.name);
    }
}

The above is hopefully familiar if you’ve used classes before in another language. The syntax is a little different, but our Person class has one field, name, and one method speak().

Back in the ES5 days we had to write this the old-fashioned way.

function Person {
    this.name = name;
}

Person.prototype.speak = function speak() {
    console.log('Hello, my name is ' + this.name);
}

These snippets of code are equivalent. The ES6 version is just syntactic sugar. In both cases we can create an instance of the Person class by writing: new Person('some name').

Calling methods on an object

So we’ve got our Person class. Lets create an instance and call a method on it to see what happens:

let bob = new Person('Bob');
bob.speak();
Hello, my name is Bob

Great! That’s just what we expected. However what if we try to do something a little more complex.

let bob = new Person('Bob');
let bobSpeak = bob.speak;
bobSpeak();
TypeError: Cannot read property 'name' of undefined

Boom! That didn’t work.

The reason is that bob actually doesn’t have a speak. He inherits the speak property from his class, Person. That means our bobSpeak method is actually no longer linked to bob at all. In fact bobSpeak === Person.prototype.speak.

When you write let bobSpeak = bob.speak the JavaScript runtime looks for a property called speak on the bob object. Because it’s not there JavaScript checks on bob‘s prototype (bob.__proto__). That’s Person, which contains a property with the name speak, so it is returned. If speak was not found JavaScript would keep looking back through the class inheritance hierarchy recursively until it finds a property or it reaches Object.prototype.

How do we make it work? Use Function.call or Function.bind.

let bob = new Person('Bob');
let personSpeak = bob.speak;
personSpeak.call(bob);
let bobSpeak = bob.speak.bind(bob);
bobSpeak();

Passing methods around as parameters

JavaScript has first class functions, so often we pass them around as parameters. This doesn’t work quite right with methods, here’s an example.

function logAndRun(fn) {
    console.log('Beginning function at ' + new Date());
    fn();
    console.log('Ending function at ' + new Date());
}

let bob = new Person('Bob');
logAndRun(bob.speak);

This fails for the same reason described above. Inside logAndRun the fn variable is actually pointing to Person.prototype.speak. The reference to bob has been completely lost, so when the speak() code runs it has this === undefined.

The fix is the same as before.

let bob = new Person('bob');
logAndRun(bob.speak.bind(bob));

Unfortunately JavaScript cannot do this for you automatically. When you write bob.speak() it knows you’re doing a function call, so it can automatically bind the this keyword. However if you just write bob.speak it cannot know if you intend make a method call with that value at some point in the future.

Anonymous functions (closures)

Lets enhance our Person class so that Bob can speak about his hobbies instead of just his name.

class Person {
    constructor(name, hobbies) {
        this.name = name;
        this.hobbies = hobbies;
    }

    speak() {
        // WILL NOT WORK
        this.hobbies.forEach(function(hobby) {
            console.log(this.name + ' likes to ' + hobby);
        });
    }
}

Unfortunately the code above doesn’t work. Every time you call a new function(){} the this keyword is reset to undefined.

The simplest workaround is to save a copy of the this keyword.

speak() {
    let self = this;
    this.hobbies.forEach(function(hobby) {
        console.log(self.name + ' likes to ' + hobby);
    });
}

You can also bind the method.

speak() {
    this.hobbies.forEach((function(hobby) {
        console.log(this.name + ' likes to ' + hobby);
    }).bind(this));
}

However the best solution is to use ES6 arrow functions. These don’t rebind the this variable, so this behaves like a normal variable and keeps whatever value it had in the surrounding scope.

speak() {
    this.hobbies.forEach((hobby) => {
        console.log(this.name + ' likes to ' + hobby);
    });
}

Keeping safe

Here are a few practical tips:

  • Always put ‘use strict’ at the top of your JavaScript files.
  • Always use ES6 arrow functions for your closures, for example: myArray.forEach((x) => console.log(x));
  • Prefer named functions at the top-level, for example: function doThing() { }
  • When passing a function as a parameter, like in logAndRun(foo.doThing), if you constructed foo from a class then you’ll need to use Function.bind.
  • Use eslint with the prefer-arrow-callback option. Check out my .eslintrc.