The THIS Keyword

As you know, the this keyword in JS OOP refers to the current object within a constructor or a method. In this reading, we'll look at a few surprising and subtle aspects of this, but we're not going to cover every quirk of it. If you want to know more about this you can explore more in that link.

Changeability

An important thing to keep in mind about this is that it's constantly changing. Every time there's a function call or a method call or an object construction, the value of this changes. Most of the time, the new value is either irrelevant or exactly what you want. So, there's no cause for alarm.

Constructor

If we use a constructor

var harry = new Account(50);

Then the value of this in the body of the constructor function is the new object (the one that will be assigned to harry above):

class Account {
    // our instance variable
    balance = 0;
    // the value of `this` is the new object
    constructor (init) {
        this.balance = init;
    }
}

That's exactly what we want.

Methods

If we use a method

harry.deposit(100);

then the value of this in the body of the method is the object that the method is operating on:

class Account {
    // our instance variable
    balance = 0;
    constructor (init) {
        this.balance = init;
    }
    // the value of `this` is the object to work with
    deposit (amt) {
        this.balance += amt;
    }
}

Again, this is exactly what we want.

Methods are Functions

So far, we've only invoked methods. The dot syntax, though, allows us to pull a value out of an object, and a method is a value in an object (kinda -- there's a subtlety here that we can skip for now). Return to our bank page and try the following code:

harry.balance;
harry.deposit;

Here's a screenshot of how it worked for me:

harry balance and deposit

So, deposit is a kind of function, but a function stored in an object. Actually, to be precise, the function is stored in something called a prototype, which all instances of a class inherit from. That means that Harry's deposit method is the very same function as Ron's and Hermione's deposit methods. But prototypes are not a source of concern.

Stripped to its essentials, the deposit method (all of them; they're all the same) is:

function (amt) {
    this.balance += amt;
}

So, it will work fine as long as there's a value for this.

As you know, when we use the method properly, the value of this is set correctly:

ron.deposit(10);

Increments the balance variable in Ron's bank account.

Remember, a method is just a function that belongs to a particular class of object, like deposit and withdrawal belong to Account.

Methods as Arguments

But we know from earlier work (such as the Plotting assignment) that functions can also be arguments, as with the .map() method.

Suppose a group of friends decides to help Ron out, passing the hat to collect money for him (maybe to go home for the holidays or to get a new broom or dress robes). We could implement code like this:

console.log(ron.balance);
var hat = [10, 20, 30, 40]; // total of 100
hat.map(function (amt) { ron.deposit(amt) });
console.log(ron.balance);

This works spectacularly well. (You can copy/paste that code into the console for the bank page and test it. Ron's balance increases by 100.)

But note that the argument to map above is an ordinary function, not a method. What if it were a method? Try the following:

console.log(ron.balance);
var hat = [10, 20, 30, 40]; // total of 100
hat.map(ron.deposit);
console.log(ron.balance);

That fails. It fails because the value of this is incorrect. But the earlier code with the anonymous function works fine, so let's stick with anonymous functions.

Functions inside Methods

In fact, we decide to get ambitious. We decide that the passing the hat code is useful and generic, and it might be nice to have a method that makes a bunch of deposits into a bank account. Here's our attempt:

class Account {
    ...
    depositAll (hat) {
        hat.forEach(function (amt) { this.deposit(amt) });
    }
    ...
}

Try it out at account2 with code like:

console.log('before', ron.balance); 
ron.depositAll([1,2,3,4]);
console.log('after', ron.balance);

The code fails because the anonymous function (the argument to map) doesn't have the correct value for this. A named function would fail for the same reason. Both would fail because it is called (by .map) as an ordinary function, not as a method, and ordinary functions get a different value for this. (The value it gets varies depending on JavaScript implementation and context, but it's never the right value. In a browser, it's the global object, which we met as globalThis.)

Arrow Functions as a Solution

There are many solutions1, but the easiest is to use an arrow function instead of a normal function.

Remember that an arrow function, in it's most concise form, consists of a parameter list, an arrow, and an expression. For example:

xValues = [1, 2, 3, 4];
doubles = xValues.map(function (x) { return x*2; });
triples = xValues.map( x => x*3 );

You can see that the arrow function is much more concise. (If we have multiple parameters, we use parentheses around them, and if we have a multi-statement function body, we surround it with braces and bring back the return keyword, if necessary.)

When we learned about arrow functions, we only learned about them as a concise alternative to normal functions, but there also are a few technical differences between them and normal functions:

  • Invoking a normal function changes this
  • Invoking an arrow function does not change this

Note that the second fact means that we cannot use arrow functions as methods. But we can use them inside methods! So, we can change the depositAll method to the following:

class Account {
    ...
    depositAll (hat) {
        hat.map(amt => this.deposit(amt));
    }
    ...
}

That works! The this inside the arrow function is the same value it normally has in the method, which means it still correctly refers to the bank account.

Try it out at account3 with code like:

console.log('before', ron.balance); 
ron.depositAll([1,2,3,4]);
console.log('after', ron.balance);

Summary

  • Most of the time, this has exactly the value you need
  • Anonymous functions inside methods might not have the correct value for this
  • Invoking an arrow function doesn't change the value of this so they are a better choice inside a method
  • Arrow functions cannot be methods, because invoking them doesn't change the value of this

  1. other solutions include using lexical closures over that, and the bind method that was created for just this purpose. I'm happy to discuss these with you, but I don't want to say more now for fear of increasing confusion.