JavaScript: How this Really Works

  • The purpose of this is to provide (or maintain) context to a function.
  • This does not refer to the function itself.
  • This does refer to the owner of a function.
  • This uses a strict set of rules to determine its context.

The ambiguous this

Note: To maintain clarity (and sanity) I will always use this when referencing code. Everything else will be referred to as: this, “this,” or this.

The keyword this is likely one of the most misunderstood (and misused) properties in all of JavaScript (JS). It’s no wonder either, considering the similarities it shares with its English counterpart – this. While they are both used for a similar purpose (providing context to something), how you derive that context is quite different.

Why we have this

If we break this down to its most basic level, it simply allows us to use a single function on many different objects.

Here’s an example.

%MINIFYHTML0d0afd3c2c5f18cb7692227ef6e40fa318%
function getNames() { return this.firstName + ' ' + this.lastName; } function nameList() { let list = "Name: " + getNames.call(this); console.log(list) } const names = { firstName: "Bilbo", lastName: "Baggins" } const moreNames = { firstName: "Frodo", lastName: "Baggins" } nameList.call(names) // Output: Name: Bilbo Baggins // because our context is “name” nameList.call(moreNames) // Output: Name: Frodo Baggins // because our context changed to “moreNames”

As you can see, we’re able to reuse these functions as many times as needed – all we have to do is change the context of the function itself. Without this, we’d have to write a whole new function any time we want to pull a different name. Now imagine if we had to pull a few thousand names!

Mistakes with this

Before we dive too deeply into what this is doing, let’s first take a moment to examine what it’s not doing.

Here’s an example of how failing to understand this can leave people frustrated.

function coffee(number) { console.log("Cups of coffee: " + number); // Don’t do this this.total++; } coffee.total = 0 let i; for (let i = 0; i < 10; i++) { if (i > 0) { coffee(i); } } // Cups of coffee: 1 // ... // Cups of coffee: 9 // Great! It's keeping track of our coffee addiction. console.log(coffee.total); // Output = 0 ...wait what?

While it would seem that this should refer to the function itself, that isn’t the case. What’s really happening here is that this is pointing to the owner of the function.

Naturally, your next question should be: “Then what is the owner of our function?”

If a function hasn’t been given any context (this will be covered later), the default owner of that function is the global scope or window of the program. As you can imagine, using the global scope as a reference will cause all sorts of issues.

To give you an example of this, the above code has actually created a new global variable called total, with a value of NaN. Here it is action:

See the Pen Example of how to “not” use this. by yamato53 (@yamato53) on CodePen.

Note: You can prevent this from using the global scope by adding strict mode to your code. With strict mode enabled, this will throw an error of undefined instead of interacting with the top level of the program.

At this point, most people will start looking for an easy solution – such as this:

The work-around “solution”

function coffee(number) { console.log("Cups of coffee: " + number); // we want this to keep track of our coffee intake // so we provide a specific place to store our counter information. coffeeCounter.total++; } let coffeeCounter = { // then we create a “holding place” for the total. total: 0 } coffee.total = 0; // coffee.total uses the coffee function to // determine where it's saving the total number of cups at. let i; for (let i = 0; i < 10; i++) { if (i > 0) { coffee(i); } } // Cups of coffee: 1 // ... // Cups of coffee: 9 console.log(coffeeCounter.total); // Output: 9

This example is like finding our car is leaking oil and instead of patching the leak, we top off the oil tank and call it fixed.

Sure, “technically” it will work – but we’ve still got a big problem under the hood and we don’t when it’s going to break again.

Proper use of this

So with all that in mind, what’s the real solution to our coffee woes? We can use a built-in method named call() to assign the proper context to our coffee function.

function coffee(number) { console.log("Cups of coffee: " + number); this.total++; } coffee.total = 0 // tracks our coffee total let i; for (let i = 0; i < 10; i++) { if (i > 0) { coffee.call(coffee, i); // forces the coffee function to maintain // its context for this.count } } // Cups of coffee: 1 // ... // Cups of coffee: 9 console.log(coffee.total); // Output: 9 - yay coffee

We’ll learn more about call() and a few other methods later on. For now, just know that JS does give us the tools we need to properly use this.

What this means

The purpose of this is surprisingly similar to the use of pronouns in normal languages. Here’s an example:

 “Jill is eating her lunch.” 

Here, we’ve set a clearly defined object (Jill), and then use a pronoun (her) to provide clarity to the sentence structure. Now compare that same sentence without the pronoun:

“Jill is eating Jill’s lunch.”

As you can see, without the help of proper pronouns, it’s difficult to tell what this example is even saying. Similarly, this is used to provide specific context to JavaScript functions.

How to set the context of this

There are a few different ways we can set ownership, with the most straight-forward solution utilizing an object’s scope. Instead of messing with methods and other shenanigans, we can “contain” this within the object we want to use.

Here’s how that looks in action:

const schoolbus = { children: 20, driver: 1, // placing the function inside of an object automatically // sets the context for this. getTotal: function() { let capacity = this.children + this.driver; return capacity; } } schoolbus.getTotal() // Output: 21

As you can see, we’ve given ownership to a function simply by placing it within the object. Because the function is “contained” it will always use that object as its context – this is referred to as using lexical scope.

Info: You’ll often see lexical scope used in a coding style known as “Principle of Least Privilege”. The purpose of this style is to “hide” anything from the global scope that isn’t completely necessary.

That being said, what if we’re working outside of the constraints of an object? Or maybe we want to create a function that can be reused on many different objects? Luckily, we have answers to those questions as well – but first, we need to understand a few more things about this.

Why this can be confusing

Is this starting to make sense? Great! Now it’s time to throw another curveball.

const restaurant = { food: 'cheeseburgers', menu: function() { console.log(this.food) } } restaurant.menu() // ^ ^ // Context | Reference // Output: cheeseburgers

This code seems simple enough, if we break it down it’s doing three things:

  1. We have the main object restaurant.
  2. We have a reference to the restaurant’s menu.
  3. We’re given the menu’s food information – cheeseburgers.

Now, let’s go one step deeper.

‘use strict’ // enabling strict mode to catch any errors. const restaurant = { food: 'cheese burgers', menu: function() { console.log(this.food) } } $('button').click(restaurant.menu) // ^ ^ ^ // Context | References // Output: Error - undefined

Wait a sec, why is there an error?

To put it crudely – most native JS methods (such as .click) do not literally pass in their arguments (restaurant and menu). Instead, it turns these items into a reference. Consequently, because restaurant is being used as a reference instead of the main context, this no longer knows what it’s supposed to do!

Think of it like saying “Someone is eating her sandwich.” – because we lost our main subject, we no longer know who “her” is referring to.

This begs the question – how do we retain context when using methods?

Methods for this

JavaScript provides us with four options we can use to give context to this:

We’ll start with the new operator first, as it as the highest seniority of the four.

New object binding

When an object is created using the new operator, this will always refer to the newly created object.

Here’s how that looks in practice:

function Computer(brand, type) { this.brand = brand; this.type = type; } const newComputer1 = new Computer('Dell', 'desktop') const newComputer2 = new Computer('Apple', 'laptop') console.log(newComputer1) // Output: { brand: 'Dell', type: 'desktop' } console.log(newComputer2) // Output: { brand: 'Apple', type: 'laptop' }

As you can see, any objects created with new are always set as the main context. This allows us to use a single universal function (also known as a constructor) to build any number of objects. 

Bind()

Where new is used to create a new object, bind() is used to create a new function, that has its this value bound to the object we pass in. In other words, we’re saying “this must always use the context we give it.”

First, let’s take a look at an incorrect example. Then, we’ll see how bind() can be used to fix it.

const basket = { apples: 2, getApples: function() { return `I have ${this.apples} apples!` } } const showApples = basket.getApples console.log(showApples()) // Output: "I have undefined apples!" Uh oh...

Why won’t this lovely program return how many apples we have in our basket? 

Simply put, showApples is pulling getApples out of its cozy basket object. Since the getApples() function is no longer “inside” the basket object, it loses its context.

However, with one simple line of code, we can solve our apple conundrum.

const basket = { apples: 2, getApples: function() { return `I have ${this.apples} apples!`; } } var showApples = basket.getApples.bind(basket) console.log(showApples()) // Output: "I have 2 apples!"

As you can see, the showApples() function is now bound to our basket object. We can call it as many times as we need, and it will always have the same context.

Call() and Apply()

If bind() is meant for long-term use, then consider call() and apply() the short-term solution.

We can use these two methods if we need to do something with an object – but have no plan to reuse it in the future. In other words, the context provided to call and apply will be forgotten after being used.

Here’s an example:

function fruits() { // .join is just adding spaces between each item in our array. let answer = [this.fruitOne, "and", this.fruitTwo, "are my favorite fruits!"].join(' ') console.log(answer) } let favoriteFruits = { fruitOne: 'apples', fruitTwo: 'bananas' } fruits.call(favoriteFruits) // Output: "apples and bananas are my favorite fruits!"

Much like the previous two methods, call() is providing our function with the right context for this. The difference here is that call() won’t waste resources by saving this to a new function or object.

Note: Apply() works exactly the same way as call() – the only difference being call() accepts a list of arguments, whereas apply() accepts an array of arguments. You can read more about this subtle difference from the Mozilla dev docs.

Some keynotes:

  • Bind() is used to assign a specific owner to a function, no matter how many times that function is reused.
  • Call() and apply() are meant to be used immediately. The context these methods provide can only be used in that specific instance.
  • Bind, call and apply can not be used together.

Take-away

As we’ve seen, this doesn’t warrant all of the frustration that often comes with it. Much like a game of chess – if you don’t understand how each piece works, you’re going to have a hard time playing the game.

If you’re still a bit fuzzy on the whole subject, just focus on these four simple rules:

  1. New will always set this to the newly created object.
  2. Bind(), call() and apply() will set this to the object you’ve specified.
  3. Using this within an object forces it to use that object as the owner.
  4. If none of the above apply, this will use the global scope as its context (or throw an undefined error if in strict mode).

That’s it! Following these guidelines will keep you from getting tripped up by the (seemingly) strange behavior of this.

If you’d like to take a deeper dive into the “hard parts” of JS (which I highly recommend you do!) check out this book series – You Don’t Know JS. You’ll come to appreciate how all these weird systems work together to create the strange language that we all love to hate – JavaScript!

Dakota

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.