A different way of understanding this in JavaScript

[2017-12-06] dev, javascript
(Ad, please don’t block)

In this blog post, I take a different approach to explaining this in JavaScript: I pretend that arrow functions are the real functions and ordinary functions a special construct for methods. I think it makes this easier to understand – give it a try.

Two kinds of functions  

In this post, we focus on two different kinds of functions:

  • Ordinary functions: function () {}
  • Arrow functions: () => {}

Ordinary functions  

An ordinary function is created as follows.

function add(x, y) {
    return x + y;
}

Each ordinary function has the implicit parameter this that is always filled in when it is called. In other words, the following two expressions are equivalent (in strict mode).

add(3, 5)
add.call(undefined, 3, 5);

If you nest ordinary functions, this is shadowed:

function outer() {
    function inner() {
        console.log(this); // undefined
    }

    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

Inside inner(), this does not refer to the this of outer(), because inner() has its own this.

If this were an explicit parameter, the code would look as follows:

function outer(_this) {
    function inner(_this) {
        console.log(_this); // undefined
    }

    console.log(_this); // 'outer'
    inner(undefined);
}
outer('outer');

Note that inner() shadowing the this of outer() is also how variables work in nested scopes:

const _this = 'outer';
console.log(_this); // 'outer'
{
    const _this = undefined;
    console.log(_this); // undefined
}

Due to ordinary functions always having the implicit parameter this, a better name for them would have been “methods”.

Arrow functions  

An arrow function is created as follows (I’m using a block body so that it looks similar to a function definition):

const add = (x, y) => {
    return x + y;
};

If you nest an arrow function inside an ordinary function, this is not shadowed:

function outer() {
    const inner = () => {
        console.log(this); // 'outer'
    };
    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

Due to how arrow functions behave, I also occasionally call them “real functions”. They are similar to functions in most programming languages – much more so than ordinary functions.

Note that the this of an arrow function can not be influenced via .call(). Or in any other way – it is always determined by the scope surrounding the arrow function when it was created. For example:

function ordinary() {
    const arrow = () => this;
    console.log(arrow.call('goodbye')); // 'hello'
}
ordinary.call('hello');

Ordinary functions as methods  

An ordinary function becomes a method if it is the value of an object property:

const obj = {
    prop: function () {}
};

One way of accessing the properties of an object is via the dot operator (.). This operator has two different modes:

  • Getting and setting properties: obj.prop
  • Calling methods: obj.prop(x, y)

The latter is equivalent to:

obj.prop.call(obj, x, y)

You can see that, once again, this is always filled in when ordinary functions are invoked.

JavaScript has special, convenient, syntax for defining methods:

const obj = {
    prop() {}
};

Common pitfalls  

Let’s take a look at common pitfalls, through the lens of what we have just learned.

Pitfall: accessing this in callbacks (Promises)  

Consider the following Promise-based code where we log 'Done', once the asynchronous function cleanupAsync() is finished.

// Inside a class or an object literal:
performCleanup() {
    cleanupAsync()
    .then(function () {
        this.logStatus('Done'); // (A)
    });
}

The problem is that this.logStatus() in line A fails, because this doesn’t refer to the this of .performCleanup() – it is shadowed by the this of the callback. In other words: we have used an ordinary function when we should have used an arrow function. If we do so, everything works well:

// Inside a class or an object literal:
performCleanup() {
    cleanupAsync()
    .then(() => {
        this.logStatus('Done');
    });
}

Pitfall: accessing this in callbacks (.map())  

Similarly, the following code fails in line A, because the callback shadows the this of method .prefixNames().

// Inside a class or an object literal:
prefixNames(names) {
    return names.map(function (name) {
        return this.company + ': ' + name; // (A)
    });
}

Again, we can fix it by using an arrow function:

// Inside a class or an object literal:
prefixNames(names) {
    return names.map(
        name => this.company + ': ' + name);
}

Pitfall: using methods as callbacks  

The following class is for a UI component.

class UiComponent {
    constructor(name) {
        this.name = name;

        const button = document.getElementById('myButton');
        button.addEventListener('click', this.handleClick); // (A)
    }
    handleClick() {
        console.log('Clicked '+this.name); // (B)
    }
}

In line (A), UiComponent registers an event handler for clicks. Alas, if that handler is ever triggered, you’ll get an error:

TypeError: Cannot read property 'name' of undefined

Why? In line A, we have used the normal dot operator, not the special method call dot operator. Therefore, the function stored in handleClick becomes the handler. That is, roughly the following things happen.

const handler = this.handleClick;
handler();
    // same as: handler.call(undefined);

As a consequence, this.name fails in line B.

So how can we fix this? The problem is that the dot operator for calling a method is not simply a combination of first reading the property and then calling the result. It does more. Therefore, when we extract a method, we need to provide the missing piece ourselves and fill in a fixed value for this, via the function method .bind() (line A):

class UiComponent {
    constructor(name) {
        this.name = name;

        const button = document.getElementById('myButton');
        button.addEventListener(
            'click', this.handleClick.bind(this)); // (A)
    }
    handleClick() {
        console.log('Clicked '+this.name);
    }
}

Now, this is fixed and won’t be changed via normal function calls.

function returnThis() {
    return this;
}
const bound = returnThis.bind('hello');
bound(); // 'hello'
bound.call(undefined); // 'hello'

Rules for staying safe  

The easiest way of avoiding problems with this is by avoiding ordinary functions and always using either method definitions or arrow functions.

However, I do like function declarations syntactically. Hoisting is also occasionally useful. You can use those safely if you don’t refer to this inside them. There is an ESLint rule that helps you with that.

Don’t use this as if it were a parameter  

Some APIs provide parameter-like information via this. I don’t like that, because it prevents you from using arrow functions and goes against the initially mentioned easy rule of thumb.

Let’s look at an example: the function beforeEach() passes an API object to its callback via this.

beforeEach(function () {
    this.addMatchers({ // access API object
        toBeInRange: function (start, end) {
            ···
        }
    });
});

This function could easily be rewritten:

beforeEach(api => {
    api.addMatchers({
        toBeInRange(start, end) {
            ···
        }
    });
});

Further reading