Arrow Functions in JavaScript

Introduction

If you are a JavaScript developer, you may know that JavaScript conforms to the ECMAScript (ES) standards. The ES6, or ECMAScript 2015 specifications, had introduced some of the revolutionary specifications for JavaScript, like Arrow Functions, Classes, Rest and Spread operators, Promises, let and const, etc.

In this tutorial, we'll focus on Arrow functions, which is very much confusing and intimidating for JavaScript beginners.

Arrow Function Syntax

As we know, an ES5 function has the following syntax:

function square(a) {
    return a * a;
}

In ES6, we can write the same function with only one line of code:

let square = (a) => { return a * a; }

Furthermore, if the function body has only one statement that it returns, we can skip curly braces {} and the return statement:

let square = (a) => a * a

Also, if the function takes only one parameter we can even skip the braces () around it:

let square = a => a * a

On the other hand, if the function does not takes any parameters we may write it like this:

let results = () => { /* ...some statements */ };

We have to write less code with this syntax while providing the same functionality, which can help to declutter and simplify your code.

The advantage of this syntax is most noticeable when used in callbacks. So a verbose and difficult-to-follow snippet of code like this:

function getRepos() {
    return fetch('https://api.github.com/users/stackabuse/repos')
      .then((response) => {
          return response.json();
      }).then((response) => {
          return response.data;
      }).then((repos) => {
          return repos.filter((repo) => {
              return repo.created_at > '2019-06-01';
          });
      }).then((repos) => {
          return repos.filter((repo) => {
              return repo.stargazers_count > 1;
          });
      });
}

can be reduced to the following, by using arrow functions:

function getRepos() {
    return fetch('https://api.github.com/users/stackabuse/repos')
      .then(response => response.json())
      .then(response => response.data)
      .then(repos => repos.filter(repo => repo.created_at > '2019-06-01'))
      .then(repos => repos.filter(repo => repo.stargazers_count > 1));
}

Benefits of Arrow Functions

There are two major benefits of using Arrow functions. One is that it's a shorter syntax and thus requires less code. The main benefit is that it removes the several pain points associated with the this operator.

No Binding of 'this' Operator

Unlike other Object-oriented programming languages, in JavaScript (before arrow functions) every function defined its reference of this and it depends on how the function was called. If you have experience with modern programming languages like Java, Python, C#, etc., the operator this or self inside a method refers to the object that called the method and not how that method is called.

Programmers always complained that using this is too complicated in JavaScript. It causes great confusion in the JavaScript community and causes unintended behavior of the code in some cases.

To better understand the benefit of Arrow functions, let us first understand how this works in ES5.

The 'this' Operator in ES5

The value of this is determined by a function's execution context, which in simple terms means how a function is called.

What adds more to the confusion is that every time the same function is called, the execution context can be different.

Let us try to understand it with the help of an example:

function test() {
    console.log(this);
}
test();

The output of this program would be the following in a browser console:

Window {...}

As we called test() from the global context, the this keyword refers to the global object which is a Window object in browsers. Every global variable we create gets attached to this global object Window.

For example, if you run the following code in a browser console:

let greetings = 'Hello World!';
console.log(greetings);
console.log(window.greetings)

you will be greeted with 'Hello World!' twice:

Hello World!
Hello World!

As we can see the global variable greetings is attached to the global object window.

Let us take a different example with a constructor function:

function Greetings(msg) {
    this.msg = msg;
};

let greetings = Greetings('Hello World!');
console.log(greetings);

We will get the following message in the console:

undefined

which makes sense because we are calling Greetings() in window context and like the previous example this refers to the global object window and this.msg has added msg property to the window object.

We can check it if we run:

window.msg

We will be greeted with:

Hello World!

But if we use the new operator while creating the Greetings object:

let greetings = new Greetings('Hello World!');
console.log(greetings.msg);

We will be greeted with:

Hello World!

We can see the this operator does not refer to the global window object this time. It is the new operator that does this magic. The new operator creates an empty object and makes the this refer to that empty object.

I hope now you are getting a feeling for our earlier statement

this depends on how a function is called.

Let us take another example:

let greetings = {
    msg: 'Hello World!',
    greet: function(){
        console.log(this.msg);
    }
}

greetings.greet();

If we run this code in a browser console, again we'll be greeted with:

Hello World!

Why do you think this did not refer to the window object this time?

Implicit binding: this keyword is bound to the object before the dot.

In our example, it refers to greetings object.

But if we assign the function reference greetings.greet to a variable:

let greetRef = greetings.greet;
greetRef();

We'll be greeted with:

undefined

Does that explain anything? Remember calling a function in a window context from the previous example?

As we are calling greetRef() in a window context, in this case this refers to the window object and we know it does not have any msg property.

Let us take a more complicated scenario of using an anonymous function:

let factory = {
    items: [5, 1, 12],
    double: function(){
        return this.items.map(function(item, index){
            let value = item*2;
            console.log(`${value} is the double of ${this.items[index]}`);
            return value;
        });
    }
};

If we call factory.double() we'll get the following error:

Uncaught TypeError: Cannot read property '0' of undefined
    at <anonymous>
    at Array.map (<anonymous>)

The error indicates that this.items is undefined which means this inside the anonymous function map() is not referring to the factory object.

These are the reasons why we call that the value of this is determined by a function's execution context. There are more complicated examples on this pain point which is beyond the scope of this tutorial.

There are several ways to get around this issue like passing the this context or using bind(). But using these workarounds makes the code complicated and unnecessarily bloated.

Thankfully, in ES6 the arrow functions are more predictable in terms of reference to the this keyword.

The 'this' Operator and Arrow Functions in ES6

First, let us convert the anonymous function inside map() to an arrow function:

let factory = {
    items: [5, 1, 12],
    double: function(){
        return this.items.map((item, index) => {
            let value = item*2;
            console.log(`${value} is the double of ${this.items[index]}`);
            return value;
        });
    }
};

If we call factory.double() we'll be greeted with:

10 is the double of 5
2 is the double of 1
24 is the double of 12
[10, 2, 24]

As we can see the behavior of this inside an arrow function is quite predictable. In arrow functions this will always take its value from the outside. In fact, the arrow function does not even have this.

If we refer to this somewhere in the arrow function the lookup is made exactly the same way as if it were a regular variable - in the outer scope. We also call it Lexical scope.

In our example, this inside the arrow function has the same value as the this outside i.e in the method double(). So, this.items in the arrow function is the same as this.items in the method double() and it is factory.items.

Arrow functions can not be called with the new operator

As the arrow function does not have this keyword, it is obvious that they cannot support the new operator.

Conclusion

Arrow functions could be confusing to start with but it is super useful to make the code behave more predictable with the lexical scoping of the this keyword. It is also easy on fingers as it let you type less code.

As a JavaScript developer, you should be comfortable using it as it is being used extensively with different frontend frameworks/libraries like React, Angular, etc. I hope this tutorial will be able to help you decide when and how to implement arrow functions in your future projects.

Author image
Ireland Twitter Website
A Software Engineer who is passionate about writing programming articles. Founder of CosmoCode.io - Free Coding tutorials