Since beginning my journey to become a web developer I've kept a list. This list exists on a small cork bulletin board above my desk, connected with jewel-toned pushpins. The list is a collection of every question that has come to me in a moment when I couldn't stop and research. I call it my "All Points Bulletin Board," and the questions look like this:
- What is the visual formatting model?
- What do the properties absolute and relative mean? How do they work together?
- What is lexical scope?
- What is 7–1 CSS structure?
- What are arrow functions? How do they work?
There are many, many questions on that list now. Some have answered themselves as I learned more and worked on more projects. Others I took the time to answer through reading documentation, google-fu and books, and many more are still unanswered.
The Notion docs have piled up, and I have decided to start sharing these questions and my answers, especially if the search to answer them is particularly illuminating.
I'll be starting with the question that was the hardest for me to wrap my head around when I first started learning JavaScript:
What are arrow functions?
Question: What are Arrow Functions?
Short Answer: Basically, shortened function expressions.
Arrow functions were introduced before I became acquainted with JavaScript. I wasn't sure when I was seeing them, where to use them, and why I would even want to. In the beginning I didn't even recognize them as functions - they don't have the trademark keywords we usually see, and some don't even require parenthesis or brackets.
const multiply = (a, b) => a * b;
Arrow functions don't include the keyword function and if the code block is only one statement long, like the example above, the return keyword can be omitted. Furthermore, if only one parameter is passed in, the parentheses wrapping it can be left out.
const double = n => n * 2;
But that's not the whole story. Function expressions and arrow functions (also known as "fat arrows") can behave very differently, looking under the hood will explain why.
The Long Answer
To really break down what an arrow function is, I started by exploring the other ways functions are declared in JavaScript. Namely, function declaration and function expression.
A function created using function declaration is also known as a named function. These functions are hoisted to the top of their scope, and are called by their name. Named Functions effectively store functions until we need them.
// Function Declaration / Function Statement / Named Function
function multiplyOrDivide(a, b){
if (a % 2 === 0){
return a * b
} else {
return a / b
}
};
On the other hand, a function created using function expression is known as an anonymous function. Anonymous functions are often stored in a variable, which is how we'll eventually call them.
You'll see functions created this way used as IIFEs - Immediately Invoked Function Expressions. That's a separate blog post though.
// Function Expression / Anonymous Function
const multiply = function(a, b){return a * b};
console.log(multiply(5, 10));
// -> 50
The main difference between these two is the function name and *hoisting. *Function declarations are hoisted, function expressions are not.
**Note: In short, hoisting refers to the JavaScript compilers way of reading code - variable declarations are 'lifted' to the top of their scope and they can be called before they appear in the code. The compiler does this to preemptively create space in memory, but it can create bugs if you don't know what's going on. (Read more about hoisting Here)
Arrow Functions: Use Cases and Gotchas
Arrow functions were introduced in ES6, in large part to reduce confusion surround the this
keyword. When writing code using Promises and Callbacks, for example, the resulting code can be a maze of return and function keywords, and this can be hard to keep track of.
Use Case: Using an arrow function provides lexical this
In ES5 the complexities related to this
were worked around by creating closures, or using the (performatively slow) .bind() method. Arrow functions are a salve for this- they retain the scope of the caller inside the function. Let's look at an example:
// Function Expression / Anonymous Function
API.prototype.get = function(resource) {
var self = this; // closure created to bind this
return new Promise(function(resolve, reject) {
http.get(self.uri + resource, function(data) {
resolve(data);
});
});
};
This example was pulled from an article by Jack Pennell, where he cites Jack Franklin as the provider. You can read the whole article here.
It's a good example to visualize what we're talking about. Going into the function we immediately have to bind this
and then site it in the get method. Using an arrow function we don't have to do this extra step.
// Arrow Function / Fat Arrow Function
API.prototype.get = function(resource) {
return new Promise((resolve, reject) => {
http.get(this.uri + resource, function(data) {
resolve(data);
});
});
};
In the arrow function (that retains the scope of its caller) this
is already bound. In fact we cannot change its value. Methods like call, apply, and bind will not work.
Unlike other function types, arrow functions have no prototype property - they're more akin to a method (or an internal function) in this way. If we needed the uri in this example to be dynamic, we would not be able to use an arrow function.
Use Case: Easier to read array manipulations
ES6 came with more than just array functions (although arrow functions have been one of the most used additions), array methods like the very popular map
function, were also introduced. Using arrow functions in methods to manipulate and read arrays can arguably make them easier to read.
// Function Expression / Anonymous Function
const groceryList = [
{name: 'bananas', type: 'fruit'},
{name: 'broccoli', type: 'vegetable'},
{name: 'chicken', type: 'poultry'},
{name: 'apples', type: 'fruit'}
];
const types = groceryList.map(function(item) {
return item.type;
});
Now let's use an arrow function:
// Arrow Function
const groceryList = [
{name: 'bananas', type: 'fruit'},
{name: 'broccoli', type: 'vegetable'},
{name: 'chicken', type: 'poultry'},
{name: 'apples', type: 'fruit'}
];
const types = groceryList.map(item => {
return item.type;
});
We can simplify the function even further:
// Arrow Function
const groceryList = [
{name: 'bananas', type: 'fruit'},
{name: 'broccoli', type: 'vegetable'},
{name: 'chicken', type: 'poultry'},
{name: 'apples', type: 'fruit'}
];
const types = groceryList.map(item => item.type);
Arrow functions have been widely adopted by the developer community, and for good reason. They can help us write easier to read and maintain code - but they aren't without pitfalls, and they are not a 1:1 substitution for the function declarations and expressions we're used to.
Arrow functions don't have access to the arguments object, they have no prototype property, or other internal methods. Their scope is always bound to its caller, and cannot be changed.
You can use arrow functions when writing code that relies heavily on promises and callbacks, like in a NodeJs environment. Or when manipulating large arrays, a common task in React.
To get a better idea of when and where to use arrow functions, I recommend reading this StackOverflow thread. There many developers give insight into how their teams use (and don't use) arrow functions.
Resources:
ES6 Arrow Functions: Fat and Concise Syntax in JavaScript
When (and why) you should use ES6 arrow functions - and when you shouldn't
An Introduction to JavaScript Arrow Functions