The latest version of JavaScript (ES2015) allows us to write functions in a different way by using the arrow function syntax. This not only allows for terser function declarations but also has additional benefits.
Functions are a part of your code that implement a specific task. The most important thing to remember about functions is that they are reusable – which means that they can be called at any point in time and perform the action that they are programmed for.
Imagine that we have an application that needs to greet people, we really wouldn't want to write something like console.log('Hello [name]')
multiple times, right? In order to make our lives better we can organise this logic into a function and just call the function when needed. To stick with our example the function could look like this:
function greet(name) {
return 'Hello' + name;
}
console.log(greet('Steve')); // Hello Steve
console.log(greet('Ann')); // Hello Ann
Of course, functions are normally larger and they can be really complex, achieving advanced functionality.
Arrow functions, or sometimes also referred to as 'fat arrow' functions, allow you to define functions in a terser way in ES2015.
Take a look at this example, probably you have written something similar to this code before:
const numbers = [0, 1, 2];
numbers.map(function (number) {
return console.log(number);
});
The above can be rewritten using the arrow functions to look like the code below by removing the function keyword and replace it with the syntax of =>
. There are some additional rules around on how arguments should be handled as well as how the return
keyword is used (we'll discuss these later):
const numbers = [0, 1, 2];
numbers.map((number) => console.log(number));
this
The difference between regular JavaScript functions and these new arrow functions is not only the fact that you can have a shorter function signature but that this
is lexically bound to the calling function
This last sentence may be a bit confusing (excuse the pun) so let's take a look at how the this
keyword works in JavaScript as you (may) know it today.
Take the following, relatively simple example: we have a JavaScript object here that allows us to calculate quotients:
const quotient = {
numbers: [1, 2, 3, 4, 5, 6, 7],
results: [],
divideFn: function (divisor) {
return this.numbers.map(function (divident) {
if (divident % divisor === 0) {
return this.results.push(divident);
}
});
},
};
We have a numbers property where we store our sample dataset, we have a results property where we will place our results and finally a divideFn()
that goes through each number and make sure that the number is dividable by the parameter passed to the function itself. Notice that if the module operator (that's the %
sign) returns 0 – in other words, if the number is dividable - we'll add that number to our results array.
To run this example, add these two lines to find numbers that can be divided by 3:
quotient.divideFn(3);
console.log(quotient.results);
If we try to run this example we'll face an error, namely, we'll see a message similar to Cannot read property 'push' of undefined
. Okay, this is weird - results is definitely an array and therefore it should have a push()
method.
If we start to debug this we'll realise that the problem is with the execution context, and with the keyword this
. Try to update the code and add a console.log(this);
statement just before the second return statement in the if block.
Depending if we run this example using Node.js or our browser we'll see a different result, but nevertheless we will notice that logging the value of this
yields an object – but not our object (quotient) in this case.
Let's discuss this a bit further. Notice how we use a .map()
function that allows us to iterate through the numbers
array. .map()
then has an anonymous callback function as well and inside this function is where we have our if statement and where we utilise this
. Therefore, this
, in this case, is contextually sensitive, meaning that the value of this
is referencing the anonymous function, which in turn is added to the global scope.
There are various solutions to "this" problem in JavaScript today (excuse the pun, again). The first one is to create another variable and assign this
to it (let's also print the value of that
) – this is how the updated code could look like:
const quotient = {
numbers: [1, 2, 3, 4, 5, 6, 7],
results: [],
divideFn: function (divisor) {
let that = this;
return this.numbers.map(function (divident) {
if (divident % divisor === 0) {
console.log(that);
return that.results.push(divident);
}
});
},
};
Now we won't see the error and also the value of that
is going to be our object.
Another widely used solution is to call the .bind(
) method passing in this
as the argument. Essentially the .bind()
method creates a new function scope and therefore it will have the this
keyword set to a value provided. Let's try this out as well, and we should see the same result as before:
const quotient = {
numbers: [1, 2, 3, 4, 5, 6, 7],
results: [],
divideFn: function (divisor) {
return this.numbers.map(
function (divident) {
if (divident % divisor === 0) {
return this.results.push(divident);
}
}.bind(this)
);
},
};
Even though these are all good solutions, when using the ES2015 variant of JavaScript we don't need to use any of these. Using the arrow functions will keep the value of this
lexically bound to the calling function. Therefore our example can be rewritten to be:
const quotient = {
numbers: [1, 2, 3, 4, 5, 6, 7],
results: [],
divideFn: function (divisor) {
return this.numbers.map((divident) => {
if (divident % divisor === 0) {
return this.results.push(divident);
}
});
},
};
Running this example will no longer throw errors and will execute correctly, with the expected end result.
This is a huge addition in my opinion to JavaScript – such anonymous callback functions are being used in popular JavaScript libraries such as jQuery where developers are facing the issues with the this
keyword quite frequently.
Let's take a look at the basic syntax rules behind the arrow functions. Just remember the following:
// return is implicit
const numbers = [0, 1, 2];
numbers.map((number) => console.log(number));
// curly brackets, use return:
numbers.map((number) => {
number *= 2;
return console.log(number);
});
You can also use arrow functions to define non-callback functions. In the following example, we can see this and also see how we can return an object by using parentheses:
const greet = (name, age) => ({
name: name,
age: age,
});
console.log(greet('Steve', 18));