The KIRUPA orange logo! A stylized orange made to look like a glass of orange juice! Tutorials Books Videos Forums

Change the theme! Search!
Rambo ftw!

Customize Theme


Color

Background


Done

Table of Contents

Arrow Functions

by kirupa   |   filed under JavaScript 101

Learn about arrow functions and their awesome superpowers in conciseness and lexical scoping-ness!

Before we move on, let’s address the elephant in the room. Arrow Functions is probably the coolest name for some technical thing ever. It makes the names for all of the other JavaScript concepts we’ve seen so far (and will see in the future) seem downright dreadful. With this important observation out of the way, let’s get down to business and learn about what they are and what makes them an upgrade over the traditional functions we have seen.

Onwards!

What are Arrow Functions?

The best way to understand arrow functions is to dive right in, start looking at examples, and observing their behavior. On the surface, arrow functions are nothing more than an abbreviated syntax on a typical function expression. There is a whole lot more to arrow functions than just that, but we’ll start there and gradually go deeper.

Starting with the Basics

Let’s say we have a traditional function that looks as follows:

let laugh = function () {
  return "Hahahaha!";
}
console.log(laugh()); // "Hahahaha!"

This function is called laugh, and it returns the text Hahahaha! when called. If we turned this into an arrow function, it will take on this more concise form:

let laugh = () => "Hahahaha!"; 
console.log(laugh()); // "Hahahaha!"

Notice what just happened:

  1. We got rid of the function keyword
  2. We specified an arrow (=>) that lives between the () and the function body
  3. The curvy brackets were removed because we don't need them if our arrow function is just a single statement. Going further, if our single statement is an expression that returns a value (which is what we have), we can remove the return keyword as well

Summarizing what just happened, we went from three lines of function-related code to just a single line. The behavior between our more verbose traditional function and the more concise arrow function is identical where calling laugh prints the same Hahahaha! to the console in both cases.

Of Arguments and Parenthesis

Building on what we just saw, if our function takes a single argument, we can remove the open/close parenthesis when defining our arrow function:

let laugh = name => "Hahahaha! " + name + "!";
console.log(laugh("Zoidberg")); // "Hahahaha! Zoidberg!"

Notice that our laugh function takes the argument name, and it returns the combination of name and Hahahaha!. This is the most concise form of an arrow function where we remove all of the syntactical fluff when we have just a single argument. If we add more arguments to our function, the parenthesis around the arguments will come back:

let laugh = (first, last) => "Hahahaha! " + first + " " + last + "!";
console.log(laugh("John", "Zoidberg")); // "Hahahaha! John Zoidberg!"

The main thing to keep in mind is that the way the parenthesis behave is identical to how you would treat them with traditional functions. We can specify as many arguments we want with a comma separating each individual argument. We can even specify default values for the arguments.

To Curly Bracket or Not to Curly Bracket

We mentioned earlier that we can omit the curly brackets if our function has only a single statement. If our function specifies multiple statements, the curly brackets have to be back:

let anotherExample = () => {
  console.log("Hello");
  console.log("Everybody");
}
anotherExample();

If our function with multiple statements is returning a value, then we have to ensure the return keyword is used as well:

let anotherExample = () => {
  let a = "Hello ";
  let b = "Everybody");
  
  return a + b;
}
anotherExample();

When we think about it, this makes a whole lot of sense. If we have multiple statements, it is hard to know which statement contains the value that we want to return. Having an explicit return avoids that confusion.

Note: Preferences May Vary!

Here’s the thing. You or your team may not particularly enjoy toggling between showing brackets or using return or not based on what exactly the function is doing or how many statements are in it. There is nothing wrong with always displaying curly brackets and using a return:

let laugh = () => {
  return "Hahahaha!";
}
laugh();

Here is an example of our laugh function from earlier where we display both the curly brackets and the return keyword despite the function body having just a single expression that returns a value. This function totally works and isn't wrong at all.

 

Putting it All Together

Just for good measure, let’s take a lot of what we have just seen and explore one last example:

let calculateDiameter = (radius = 1) => {
  let pi = 3.14159;
  let diameter = 2 * pi * radius;
  
  return diameter;
};
console.log(calculateDiameter(4)); // 25.13272
console.log(calculateDiameter()); // 6.28318

We have a function called calculateDiameter, and it takes a single argument called radius (which has a default value of 1 that is used when we don't specify an argument.) Calling the calculateDiameter function with (or without) a radius argument returns the correct value.

The biggest takeaway for us with arrow functions is this: almost anything you can do with traditional functions, you can do with arrow functions as well. Now, there are a few big differences, hence the emphasis on almost. Arrow functions and traditional functions have different scoping behavior, and arrow functions have a few limitations that we should be aware of. We’ll get to the bottom of what’s up with all this in the next few sections.

Arrow Functions and Scoping

Brace yourself. This is going to be a sordid tale involving variable scopes and the value of this. These are two topics that, on the best of days, can make anyone's head spin. The best way to understand the differences in scoping behavior between traditional functions and arrow functions is to look at a common situation we will run into.

The situation looks as follows:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(function () {
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

We have an object called counter, and it has the initial, interval, and startCounting properties. The property that we want to focus on is startCounting, and it represents a function whose body is a setInterval function:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(function () {
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

When someone calls startCounting, the idea is for us to increment the value of initial at a rate specified by the interval property.

Thinking out loud, when we call counter.startCounting(), the value of initial is initially (ha!) 100. After 1000 milliseconds (as specified by the interval property) elapses, the value of initial is incremented by 1 to be 101. After another 1000 milliseconds, the value of initial becomes 102. You get the picture.

Now, if we test this code, what do you think we are going to see? What will the console.log statement inside our interval loop show when it prints the value of initial every 1000 milliseconds? As it turns out, what we will see is not a nice set of numbers increasing by 1 starting from 100. Instead, what we will see is NaN being printed over and over again:

Why is this the case? We can see more details if we add a few more console.log statements to see what the value of this is inside thestartCounting and setInterval functions:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    console.log("startCounting:");
    console.log(this);
    setInterval(function () {
      console.log("setTimeout:");
      console.log(this);
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

If we run this code, we'll see our additional information being printed to the console. The value of this under startCounting will refer to the counter object:

The value of this inside oursetTimeout function will refer to the window object:

Here is where things are problematic. You may think that setInterval would inherit the value of this from its outer environment as defined by startCounting, but that isn't the case. The reason is that traditional functions don't behave in this seemingly logical way. They define their own value for this and that is always going to refer to the context they are being used in.

Our anonymous function inside our setTimeout doesn't get created when our counter object is initialized. It gets created only when we call the startCounting method:

counter.startCounting();

This call lives in the context of the window object. When startCounting is invoked and the anonymous function is created, the this.initial call is looking for the value of initial on the window object. That property doesn't exist there, and that is why trying to increment this non-existent variable gives us a NaN.

There are a few ways to fix this. One approach is to store the value of this and pass the stored this value into our anonymous function:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    let that = this;
    setInterval(function () {
      that.initial++;
      console.log(that.initial); // undefined
    }, this.interval);
  }
}
counter.startCounting();

Notice that we introduced a that variable in the startCounting context to store a local reference to this. Inside our anonymous function, we use that where we earlier used this. Because that is properly storing a version of this that is tied to our counter object, our use of that.initial properly resolves to the correct value. If you run this code, you'll the output of initial properly incrementing when we examine our console:

All of these mentions of this and that in this explanation can be a bit confusing, but do feel free to ask for help if you have any questions.

We just saw one approach of addressing this problem by storing the value of this and using this stored value in place of the actual this. There is a better approach. Let's say we replace our anonymous function inside our setInterval with an arrow function:

let counter2 = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(() => {
      this.initial++;
      console.log(this.initial); // works
    }, this.interval);
  }
}
counter2.startCounting();

When we run this code, what gets printed this time around is 100, 101, 102...and so on just like we want. No mess. No fuss. No that and this confusion. Why does an arrow function work here while traditional functions require extra gymnastics?

The reason is that arrow functions do not define their own this value. Instead, they inherit the value of this from where their code is defined in the document. Another way of saying this is that the this value inside arrow functions is determined by its surrounding scope, more formally called the lexical scope. This is in contrast to traditional functions whose this value comes from the context in which they are used in. This is a subtle difference with a huge (and beneficial) change in behavior.

Note: Some Things Arrow Functions Don't Have

Beyond not having their own this value (which they inherit from their surroundings), arrow functions have no constructor or prototype properties. They also don't support the bind, call, and apply methods. If you have existing code that relies on these properties and methods, your best is to stay with traditional functions or learn how to use arrow functions and adapt your code accordingly.

Conclusion

It is time to answer the inflation-adjusted million dollar question: when should we use arrow functions? There are several answers here. If you enjoy the abbreviated syntax arrow functions have compared to traditional functions, use it as much as you want. If you are someone who finds this abbreviated arrow functions syntax to lack the clarity of the more verbose traditional functions, you don't ever have to use it. My personal take is to be somewhere in the middle, like Malcolm!

Arrow functions are great for situations where we need to provide an anonymous function as part of an event handler, timer, and so on:

let myButton = document.querySelector("#myButton");
myButton.addEventListener("click", () => console.log("Click!"));

The reason is partly for the abbreviated syntax, but the other reason is mostly because of their more sensible treatment of this. As we saw a few sections ago, arrow functions' treatment of this using the lexical scope is more predictable when compared to how traditional functions redeclare this and assign it to the context they are initialized and used in.

Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!

Kirupa's signature!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2024 //--