This article attempts to explain how the this keyword works in JavaScript from a new perspective: we assume that arrow functions are real functions, and that ordinary functions are just a special language construct. I think this explanation makes this better understood, so try it out.

1. Two kinds of functions

This article focuses on two different kinds of functions.

  • Normal functions: function () {}
  • The arrow function: () => {}

1.1 Common functions

We define a normal function like this.

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

Every normal function implies a this when it is called, which means that in strict mode, the following two expressions are equivalent

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

This will be overridden (shadowed) if nested in a normal function.

1
2
3
4
5
6
7
8
9
function outer() {
    function inner() {
        console.log(this); // undefined
    }

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

This inside inner() is different from this inside outer(). inner() has its own this. Let’s assume that this is the variable declared on display, then the code would look like this

1
2
3
4
5
6
7
8
9
function outer(_this) {
    function inner(_this) {
        console.log(_this); // undefined
    }

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

inner() overrides this in outer(), a rule that also applies to nested scopes

1
2
3
4
5
6
const _this = 'outer';
console.log(_this); // 'outer'
{
    const _this = undefined;
    console.log(_this); // undefined
}

Since ordinary functions always have an implicit argument to this, it might be more appropriate to call these functions “methods”.

1.2 Arrow functions

We define an arrow function like this (I have used a block to define it so that it looks more like a normal function)

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

If you nest an arrow function inside a normal function, this will not be overridden

1
2
3
4
5
6
7
8
function outer() {
    const inner = () => {
        console.log(this); // 'outer'
    };
    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

Given this characteristic of arrow functions, I would call them “real functions”. Arrow functions are more similar to functions in most other programming languages than to normal functions. It is worth noting that the value of this in an arrow function is not even affected by .call() or anything else. this only depends on the scope in which the arrow function was created. For example

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

1.3 Common functions as methods

If an ordinary function is a property of an object, then this function is a method

1
2
3
const obj = {
    prop: function () {}
};

One way to access an object’s properties is through the dot (.) operator. This operator has two different modes.

  • Get or set a property: obj.prop
  • Calling the method: obj.prop(x, y) The latter is equivalent to.
1
obj.prop.call(obj, x, y)

As you can see, when a normal function is called, it always carries an implied this. There is also a simpler way of defining special syntax in JavaScript

1
2
3
const obj = {
    prop() {}
};

2. Common errors

We analyse the following common errors with the help of the previous points.

2.1 Error: accessing this in a callback function (in the case of a Promise)

In the code example below, which consists of a Promise, the log “Done” is hit when the asynchronous function cleanupAsync() finishes.

1
2
3
4
5
6
7
// 在某个类或对象字面量中:
performCleanup() {
    cleanupAsync()
    .then(function () {
        this.logStatus('Done'); // (A)
    });
}

The code executes to line (A) of this.logStatus() with an error. The error is that the this of this line is different from the this of .performCleanup(). The callback function’s own this overrides the outer this, meaning that we are using a normal function where we should be using an arrow function. The code works fine after switching to the arrow function.

1
2
3
4
5
6
7
// 在某个类或对象字面量中:
performCleanup() {
    cleanupAsync()
    .then(() => {
        this.logStatus('Done');
    });
}

2.2 Error: Accessing this in a callback function (in the case of .map)

Similarly, the following code will error on line (A). The reason for this is that the callback function overrides the this of the .prefixNames() method.

1
2
3
4
5
6
// 在某个类或对象字面量中:
prefixNames(names) {
    return names.map(function (name) {
        return this.company + ': ' + name; // (A)
    });
}

Again, just replace with the arrow function

1
2
3
4
5
// 在某个类或对象字面量中:
prefixNames(names) {
    return names.map(
        name => this.company + ': ' + name);
}

2.3 Error: Using methods as callbacks

Suppose you have the following UI component code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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), the UiComponent sets up the response function for the click event. Unfortunately, however, an exception is thrown when the event does fire

1
TypeError: Cannot read property 'name' of undefined

What is the reason for this? In line (A), we are using the normal property access syntax, where . is not a special method call syntax. What this means is that the function present in handleClick is set as the response function, which is roughly equivalent to the following code

1
2
const handler = this.handleClick;
handler();// 等价于: handler.call(undefined);

This causes an error in line (B) this.name. So how do we fix this? The problem here is that the dot operator doesn’t simply read the property and call the function when it makes the method call. We need to manually fill in the missing piece with .bind() after we get the method and assign a value to this, as shown in line (A)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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);
    }
}

This will fix the this problem. The value of this does not change when the function is called.

1
2
3
4
5
6
function returnThis() {
    return this;
}
const bound = returnThis.bind('hello');
bound(); // 'hello'
bound.call(undefined); // 'hello'

3. Guidelines to prevent errors

The easiest way is to avoid using normal functions and just use method definitions or arrow functions. But I still like the syntax of function definitions. Function lifting (hoisting) can be useful in some cases. If you avoid using this in a normal function you can also prevent errors. There is an ESLint rule that can help you ensure this.

3.1 Don’t use this as an argument

Some APIs like to pass some parameter-like information through this. I don’t really like this approach. It makes it impossible to use arrow functions and goes against the guidelines mentioned earlier.

As an example, in the following code, beforeEach() passes an API object through this

1
2
3
4
5
6
7
beforeEach(function () {
    this.addMatchers({ // 访问 API 对象
        toBeInRange: function (start, end) {
            ···
        }
    });
});

This function should be changed to

1
2
3
4
5
6
7
beforeEach(api => {
    api.addMatchers({
        toBeInRange(start, end) {
            // ···
        }
    });
});

4. Read more