The three faces of JavaScript`this`

JavaScript is a language which is full of surprises: its type coercion has been the subject of comedy, and one of it’s most well known books is titled JavaScript: The good parts. But of all of it’s eccentricities, the this keyword is likely the one which causes the most day-to-day confusion.

While there are a number of articles about this, they generally take an exhaustive approach to explaining the many ways of using it. This is great for reference, but not so great if you just want to know how you should use this in your next project.

That’s why in this tutorial, I explain only the three most common senses of this, by building a simplified version of a component taken from a real project – clubroom. In particular, I explain how this is used in the code which generates unique IDs for users and chat rooms.

So without further ado, let’s get started!

Say we’re building a chat app, and we want to produce unique, hexadecimal IDs for our users, starting our counter from 1000 so it looks like we’ve got a lot of users already. Let’s write the simplest possible code to accomplish this.

var userCounter = 1000;

function generateId() {
    // toString(16) converts a number to hexadecimal
    return (userCounter++).toString(16);
}

console.log(generateId()); // "3e8"
console.log(generateId()); // "3e9"

edit & run this javascript

This works great when we only have to produce unique IDs for users, but what if we also want unique IDs for our rooms? We could just repeat this code twice, but this isn’t ideal – we’d inevitably make changes to one copy of the code and forget to make changes to the other. Instead, we could try to create a single function which works with multiple counters:

var userIdCounter = 1000;
var roomIdCounter = 1000;

function generateId(counter) {
    return (counter++).toString(16);
}

console.log(generateId(userIdCounter));
console.log(generateId(userIdCounter));

console.log(generateId(roomIdCounter));
console.log(generateId(roomIdCounter));

edit & run this javascript

But this won’t work, as the counter is a Number. Every time you pass a Number to a function, JavaScript passes a copy of the number instead of the original – preventing us from modifying the original. You can check this in the linked jsbin.

We can still accomplish our goal with a similar method, though – we just need to find a way to pass the actual number into the function instead of a copy. And as luck may have it, JavaScript objects allow us to do this:

var userIdGenerator = { counter: 1000 };
var roomIdGenerator = { counter: 1000 };

function generateId(generator) {
    return (generator.counter++).toString(16);
}

console.log(generateId(userIdGenerator));
console.log(generateId(userIdGenerator));

console.log(generateId(roomIdGenerator));
console.log(generateId(roomIdGenerator));

edit & run this javascript

Having a single function which can produce a unique ID from any counter object is certainly a lot cleaner than needing a separate function for each counter, but there is still one thing we can improve.

Currently, our two counter objects and our generateId function are accessible from any other javascript code on the same page. What if a 3rd-party script we include also happens to have a function called generateId? We could give our function a funny name to try prevent collisions, but even if this works, it makes our code a lot harder to read. Instead, why don’t we attach the function to the generator object itself?

Let’s go back to an example with just the user id generator and give this a shot:

var userIdGenerator = {
    counter: 1000,
    generate: function (idGenerator) {
        return (idGenerator.counter++).toString(16);
    }
};

console.log(userIdGenerator.generate(userIdGenerator));
console.log(userIdGenerator.generate(userIdGenerator));

edit & run this javascript

Great, we’ve managed to minimise the surface area where our code can collide with 3rd-party scripts! That said, it really doesn’t feel right passing userIdGenerator as a parameter to a function which is defined on that same object. And this is where the first sense of this comes in:

1. Within a function which was called as a property of an object, this points to that object.

To put this in the context of the problem above, generate is a function which is called as a property of the object userIdGenerator. Or, in the lingo, generate is a method of userIdGenerator. This means that within the generate function, this will point to userIdGenerator.

Knowing this, can you rewrite the generate function without the parameter? Once you’re done, touch or hover over the box below for an answer.


var userIdGenerator = {
    counter: 1000,
    generate: function () {
        return (this.counter++).toString(16);
    }
};

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

edit & run this javascript

You can think of this as an invisible argument which is passed to every method, and is defined automatically by the browser to refer to the object which the method was called from.

One of the interesting things about this is that it doesn’t matter how or where the function is defined – this will still be attached to whatever object the function was associated with when it was called. For example, while we wouldn’t do this in practice, it would be possible to accomplish the same thing as above this way:

function generate() {
    return (this.counter++).toString(16);
}

var userIdGenerator = {
    counter: 1000,
    generate: generate
};

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

console.log(generate());
console.log(generate());

edit & run this javascript

Watch out though – the same flexibility which allows you to attach a function to an object and then call it as a method can also trip you up. This is demonstrated by the final calls to generate() above – did you notice that even though they are not associated with userIdGenerator, and thus did not increment userIdGenerator.counter, they still didn’t throw an error?

In fact, generate did actually run counter++ on something, but it probably isn’t what you expect. Do you know what this pointed to inside the second call to generate()? Once you’ve thought about it for a bit, touch or hover over the box below for an answer:

In a web browser,

this will point to window, as the browser runs functions which not attached to anything as if they are methods of window. Incidentally, this is why you can write both setTimeout or window.setTimeout – if you leave out the window., the browser adds it back in for you.

Now, back to our task of creating unique IDs for users and rooms without exposing the generate function to name conflicts. Given what we now know, we can do this:

var userIdGenerator = {
    counter: 1000,
    generate: function () {
        return (this.counter++).toString(16);
    }
};

var roomIdGenerator = {
    counter: 1000,
    generate: userIdGenerator.generate
};

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

console.log(roomIdGenerator.generate());
console.log(roomIdGenerator.generate());

edit & run this javascript

To avoid conflicts with other libraries, we’ve defined the generate function directly on userIdGenerator, and then to avoid repeating ourself, we’ve assigned the same function to roomIdGenerator.generate. Since this is set based on whatever is on the left hand side of the ., we’ve achieved our goal – albeit a little messily.

How could we improve this? Well, it would certainly be nicer if the definition of each object looked the same, instead of having the generate method defined on the first object and then assigned to subsequent ones. It also wouldn’t hurt if we could create more generator objects without needing to repeat ourselves by defining the counter and generate properties for each one. Happily, we can do this with the new keyword, which provides us with our second sense for this:

2. Inside a function called with the new operator, this points to a new, empty object

The javascript new operator creates a new object, i.e. {}. It then defines this inside the called function to refer to the new object. Finally, once the function has completed, new will return the newly created object (unless you manually return a separate value within the function – which you should avoid, unless you’re a masochist).

What is the new operator useful for? Well, it allows us to create functions which set up these new objects in a certain way. In fact, these “setter upper” functions have a special name – constructors, and the new objects which they create are called instances.

For example, we could use a constructor to set up instances of our “generator” objects from the previous examples. Can you write a constructor by filling out the inside of idGenerator below? Aim to make the script outputs the numbers 3e8, 3e9, 3e8, 3e9, like our previous scripts – disregard the final true/false for the moment. Test your work in the linked jsbin, and if you get stuck, view an answer by touching or hovering over the below box.

function idGenerator() {

}

var userIdGenerator = new idGenerator();
var roomIdGenerator = new idGenerator(); 

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

console.log(roomIdGenerator.generate());
console.log(roomIdGenerator.generate());

console.log(userIdGenerator.generate == roomIdGenerator.generate);

edit & run this javascript


this.counter = 1000;
this.generate = function() {
    return (this.counter++).toString(16);
};


We’re getting pretty close to our final implementation of idGenerator. In fact, the above answer actually produces the desired behaviour without any chance of bugging out. But if the last line your code outputs false, as it does in the above answer, there is one thing left to do.

It may surprise you to know that the issue in the above code did not exist in our previous answer (the one where we defined the generate function on the userIdGenerator object and then assigned it to shopIdGenerator.generate afterwards). Do you know what the problem is? Think about it for a bit, then touch or hover over the box below for an answer.

By calling new idGenerator() twice, we’ve created two identical generate functions, despite one being enough. This causes userIdGenerator.generate == roomIdGenerator.generate to be false.

This causes us to use two times the required memory. While two copies of one function aren’t going to break the memory bank, this is a bad habit to get into. For instance, if we’re calling a constructor thousands of times, each time creating 5 functions (e.g. by constructing an instance for each line in our chat), we’re going to be in trouble.

We could fix this by defining generate as a property of the idGenerator function (remembering that functions are objects too), and then assigning it to each of the produced objects:

function idGenerator() {
    this.counter = 1000;
    this.generate = idGenerator.generate;
}

idGenerator.generate = function() {
    return (this.counter++).toString(16);
};

var userIdGenerator = new idGenerator();
var roomIdGenerator = new idGenerator();

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

console.log(roomIdGenerator.generate());
console.log(roomIdGenerator.generate());

console.log(userIdGenerator.generate == roomIdGenerator.generate);

edit & run this javascript

userIdGenerator.generate and roomIdGenerator.generate are now equal! We could leave it at this, but JavaScript has an even better way of solving this – using a property of a function called it’s prototype.

What is a prototype? It is an object which can be found under the prototype property of every constructor function, defaulting to {}. While you can use it like any normal object, JavaScript has a special behaviour which makes it especially useful: if you try and access a method in an instance which doesn’t exist, JavaScript will then search the prototype of that instance’s constructor. It may help to think of a constructor’s prototype as a shared library of default methods which its instances can use.

This leads us to the third sense of this:

3. When calling a method of an object which is not defined on that object but is defined on it’s prototype, this still points to that object.

Grokking everything there is to know about prototypes takes a lot of work, but most people only need to know the basics. In fact, for this problem, all you need to know can be demonstrated with a simple example:

function idGenerator() {
    this.counter = 1000;
}

idGenerator.prototype.generate = function() {
    return (this.counter++).toString(16);
};

var userIdGenerator = new idGenerator();
var roomIdGenerator = new idGenerator();

console.log(userIdGenerator.generate());
console.log(userIdGenerator.generate());

console.log(roomIdGenerator.generate());
console.log(roomIdGenerator.generate());

console.log(userIdGenerator.generate == roomIdGenerator.generate);

edit & run this javascript

Take a few moments to try and understand this before moving on, then check that your understanding matches mine:

As

generate is attached to the prototype of the idGenerator function, you can still access it through each of the objects produced by new idGenerator – and this will still refer to the newly created objects inside of generate.

It is important that you understand this, but once you do, you can just use this simple rule of thumb for both prototype methods and the standard ones we discussed earlier:

When a function is called as a method (i.e. on the right hand side of a . character), this will by default refer to whatever is on the left hand side of the dot.

You may have noticed the words “by default” and started worrying a little – does this mean that this might refer to something else? Indeed it does, but you don’t have to worry! this will only take on a different behaviour when you want it to. Learn how to control what this points to (and why you’d ever want to do so) in the next instalment of Demystifying JavaScript!

I will send you useful articles, cheatsheets and code.

I won't send you useless inbox filler. No spam, ever.
Unsubscribe at any time.

Useful links

Want to read more about this? You may find these helpful:

Leave a Reply

Your email address will not be published.