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 complain 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 the 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
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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 are 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 lets 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.