This question already has an answer here:
-
Arrow function vs function declaration / expressions: Are they equivalent / exchangeable?
3 answers
I am curious about ES6 arrow functions (fat arrow functions). Are they simply syntactic sugar derived from CoffeeScript, or is there more to them than meets the eye?
ES6 Arrow functions in depth
One of the prettiest features of ES6, it could easily win a beauty contest, if such a contest would be held. What many people don’t know is that the arrow function is not simply a form of syntactic sugar that we can use instead of the regular callback.
As I like to explain it to the people who attend my trainings/workshops, arrow functions are this
-less, arguments
-less, new.target
-less and super
-less.
Let us now get past the shorter syntax and dive deeper into the specifics of the arrow function.
Lexical-bound this
Previously, regular functions would have their this
value set to the global object if they were used as callbacks, to a new object in case they were called with the new
operator or, in the case of libraries like jQuery, they would be set to the object that triggered an event in case of event handlers, or the current element in a $.each
iteration.This situation proved very confusing even for experienced developers.
Let’s say you have a piece of code like the one below.
var obj = {
nameValue: 'default',
initializeHandlers: function() {
var nameInput = document.querySelector('#name');
nameInput.addEventListener('blur', function(event) {
this.nameValue = event.target.value;
});
}
};
obj.initializeHandlers();
The problem is that this
inside the blur
event handler is set to the global object rather than obj. In strict mode — ‘use strict’;
— you risk breaking your application because this
is set to undefined
. In order to side-step this issue we have two options:
- Convert the event handler to a function bound to the outer scope, using
Function.prototype.bind
- Use the dirty
var self = this;
expression in the initializeHandlers
function (I see this as a hack)
Both options are illustrated below.
[...]
initializeHandlers: function() {
var nameInput = document.querySelector('#name');
// more elegant but we can do better
var blurHandler = function(event) {
this.nameValue = event.target.value;
}.bind(this)
nameInput.addEventListener('blur', blurHandler);
}
[...]
[...]
initializeHandlers: function() {
var nameInput = document.querySelector('#name');
// ugly and error-prone
var self = this;
nameInput.addEventListener('blur', function(event) {
self.nameValue = event.target.value;
});
}
[...]
On the other hand, arrow functions have no internal context. They inherit their context from the outer scope. Let’s take a look at how arrow functions solve this problem.
const obj = {
nameValue: 'default',
initializeHandlers: function() {
const nameInput = document.querySelector('#name');
nameInput.addEventListener('blur', (event) => {
// this references obj instead of the global object
this.nameValue = event.target.value;
});
}
};
In our new implementation this
is a hard reference to the obj
object and doesn’t get lost due to nesting.
Lexical arguments
Have you ever tried to access the arguments
object inside an arrow function? I have, and I wasted 3 solid hours trying to figure out why do I get the arguments of the outer function instead of those of the arrow functions.
Thankfully, MDN exists, and as good practice dictates, you check the documentation at the end, when you sit in a corner, knees tucked to your chest, rocking and repeating to yourself: “I should have been a carpenter!”
Fun aside, arrow functions do not expose an arguments
object. If you try to access it, you will get the arguments of the surrounding function. In our case, given the fact that the outer function is an arrow function as well, and we have no more functions further up the chain, we will get a ReferenceError
.
const variadicAdder = (x) => {
return () => {
let args = Array.prototype.slice.call(arguments, 0);
return args.reduce((accumulator, current) => {
return accumulator + current;
}, x);
}
}
const variadicAdderOf5 = variadicAdder(5);
console.log(variadicAdderOf5(10, 11, 12));
// ReferenceError: arguments is not defined
There is no fix here, as there is nothing broken. What we can do is to return a plain function, rather than an arrow, from our variadicAdder()
.
This will give us the opportunity to access the arguments
object without an issue. The updated code will look like the one below with the only difference
that it will actually work and not throw an error.
const variadicAdder = (x) => {
return function() {
let args = Array.prototype.slice.call(arguments, 0);
return args.reduce((accumulator, current) => {
return accumulator + current;
}, x);
}
}
const variadicAdderOf5 = variadicAdder(5);
console.log(variadicAdderOf5(10, 11, 12));
// 38
To find out more about Array.prototype.reduce
, head to the Mozilla Developer Network.
Other characteristics
As I mentioned in the introductory section of this article, arrow functions have several more characteristics besides the context and the arguments.
The first thing I would like to mention is that you are unable to use the new
operator with arrow functions. As a direct implication, arrow functions also don’t have super()
. Snippets like the one below would simply throw a TypeError
.
const Person = (name) => {
this.name = name;
};
let p = new Person('John');
// TypeError: Person is not a constructor
The third characteristic, which is as well, a direct implication of the inability to use the new
operator, is the fact that arrow functions don’t have new.target
. In a nutshell, new.target
allows you to detect whether or not a function has been called as a constructor.
Arrow functions, inherit new.target
from their surrounding scope. If the outer scope is a function, and it is called like a constructor (e.g. new Person('Adrian');
), then new.target
will point to the outer function.
The Mozilla Developer Network hosts a detailed explanation on new.target
and I encourage you to check it out.
This article is also published on my blog, here: /es6-arrow-functions-in-depth/