frontend
Arrow Functions: JavaScript ES6 Feature Series (Pt 2)
When is a function not a function? When it's an arrow.
Introduction
The inspiration behind this series of posts is simple: there are still plenty of developers for whom JavaScript makes no sense sometimes — or at the very least, has seemingly odd behavior compared to other programming languages.
Since it is such a popular and widely used language though, I wanted to provide a bunch of posts about JavaScript ES6 features that I use regularly, for developers to reference.
The aim is for these articles to be short but still in-depth explanations about various improvements to the language, that I hope will inspire you to write some really cool stuff using JS. Who knows, you might even learn something new along the way. 😄
For the second post in this series, I wanted to dive into arrow functions, and how they differ from traditional function declarations and function expressions.
Function declarations
You may have heard this before, but it bears repeating: in JavaScript, functions are first-class objects, because they can have properties and methods just like any other object. What distinguishes them from other objects is that functions can be called. In brief, they are Function
objects.
From here on, I will assume you’re familiar with the general idea of functions in JavaScript, But before I talk about arrow functions, it’s worth talking a little about both function declarations (also known as function statements) and function expressions.
Function declarations are the most basic function statement we see all the time in JavaScript. It defines a function along with the specified parameters it needs to run.
Here’s an example:
Anatomy of a function declaration
function multiply(number1, number2){
return number1 * number2;
}
console.log(multiply(4, 9));
// prints 36 to the console
If you’re looking at the function declaration example above, here’s what composes the function. multiply
is the name of the function, number1
and number2
are the two parameters the function takes in, and the body of the function: return number1 * number2;
is the statement.
Function declaration traits
Function declarations have certain traits which developers need to keep in mind as they write code, because they will trip you up at one time or another — they manage to trip us all up (myself included). 🙋
1. Functions return undefined, unless otherwise specified
By default functions return undefined
. By including the return
keyword in the body, you can specify the value it returns instead.
Function declarations undefined vs. returned values
function returnsNothing(item1, item2) {
item1 + item2;
}
console.log(returnsNothing(1, 9));
// prints: undefined
function returnsSomething(item1, item2) {
return item1 + item2;
}
console.log(returnsSomething(1, 9));
// prints: 10
For my example above, the sum of item1
and item2
is what is returned from the returnsSomething()
function, while the returnsNothing()
function, although it does exactly the same addition, merely returns undefined
when the value is called with console.log()
.
2. Function declarations are hoisted
Similar to variable hoisting, which I discussed in my previous blog post, function declarations in JavaScript are hoisted to the top of the enclosing function or global scope. This means, you can use a function before it’s actually been declared in the code.
Function declaration hoisting vs. function expression not hoisting
console.log(hoistedFunction());
// prints: "Hello, I work even though I am called before being declared"
function hoistedFunction() {
return "Hello, I work even though I am called before being declared";
}
console.log(notHoisted());
// prints: TypeError: notHoisted is not a function
var notHoisted = function() {
return "I am not hoisted, so I will not be found if called before my declaration";
}
console.log(notHoisted());
// prints: "I am not hoisted, so I will not be found if called before my declaration"
In the above example, the function hoistedFunction()
returns its value, regardless of when it’s called in the code, because it’s a function declaration.
On the other hand, the second function assigned to the variable notHoisted()
, which is a function expression, is not hoisted to the top of the scope so if it is invoked before the function is parsed, it throws a TypeError
in the code that it is not a function (mainly because the compiler’s not aware of it yet).
Those are main things you need to be aware of when thinking about function declarations.
Let’s move on to function expressions.
Function expressions
Function expressions are similar to function declarations. They still have names (which are optional this time), parameters, and body-based statements.
Anatomy of a function expression
const divide = function(number1, number2){
return number1 / number2;
}
console.log(divide(15, 5));
// prints: 3
For this example, the variable divide()
is assigned to the anonymous function which takes the parameters number1
and number2
, and returns the quotient as per the body’s statement return number1 / number2;
.
Function expression traits
Just like function declarations, function expressions have some defining quirks of their own. Here’s what you need to know about them.
1. Function expressions can be anonymous (or not)
As I mentioned briefly above, function expressions, since they’re assigned to variables, can omit a name, and be what’s known as an "anonymous function". This is possible because the variable name will be implicitly assigned to the function.
Anonymous function expression (implicit naming at work)
const anonymous = function() {
return "I do not need my own name, as I am assigned to the variable anonymous";
}
console.log(anonymous());
// prints: "I do not need my own name, as I am assigned to the variable anonymous"
As the variable name implies, since the function it’s referencing has no name, it implicitly assigns anonymous()
to the function.
If, however, you want to refer to the current function inside the function body, you need to create an explicitly named function (whose name is local only to the function body).
Named function expression (explicit naming at work)
var math = {
"factit": function factorial(n) {
console.log(n)
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
};
console.log(math.factit(3));
//prints: 3; 2; 1;
For this variable math
, you can call the factorial()
function by invoking math.factit();
outside of the object, and passing the required parameter in.
I don’t find as much need for this type of named function expression in my day to day development, but it’s nice to know it’s available if the need arises.
Bottom line: If the function expression’s name is omitted, it will be given the variable name (implicit name). If the function expression’s name is present, it will be the assigned function name (explicit name).
2. Function expressions can be IIFEs
A function expression can be used as an IIFE: an Immediately Invoked Functional Expression, which runs as soon as it’s defined.
This immediate execution by the JavaScript engine is triggered by the parentheses ()
at the end of the anonymous function.
I won’t go into too much detail here, but for any variables created within the IIFE to be accessible to the outside or global scope, the anonymous function must be assigned a variable a la a function expression.
If it’s not and the anonymous function’s just invoked at run time, any variables created inside the function’s scope will be invisible to the outside world.
IIFE variables that are accessible vs. IIFEs that are not
const cogitoErgoSum = (function () {
const quote = "I think therefore I am";
return quote;
})();
// immediately creates the output
cogitoErgoSum;
// prints: "I think therefore I am"
(function (){
const quote2 = "I am not outside this IIFE";
})();
quote2;
// prints: ReferenceError: quote2 is not defined
3. Function expressions don't hoist
Function expressions (and arrow functions) take after the new let
and const
variable keywords, in that they don’t get hoisted at run time.
As I demonstrated above in the function declaration section around hoisting, function expressions do not hoist, a function expression is created when the execution reaches it and it is usable from then on.
Function declaration hoisting vs. function expression not hoisting
console.log(hoistedFunction());
// prints: "I am a function declaration so I am hoisted to the top of the scope at run time"
function hoistedFunction() {
return "I am a function declaration so I am hoisted to the top of the scope at run time";
}
console.log(stillNotHoisted());
// prints: TypeError: stillNotHoisted is not a function
const stillNotHoisted = function() {
return "I am a function expression and therefore, hoisting does not apply to me";
}
console.log(stillNotHoisted());
// prints: "I am a function expression and therefore, hoisting does not apply to me"
The outcome is the same as when I described it in function declarations, function expressions throw TypeErrors
if invoked before they are parsed at run time. Just don’t do it.
Ok, now time to move on to arrow functions: the latest and greatest function improvements courtesy of ES6.
Arrow function expressions ➡️
Arrow function expressions are syntactically compact alternatives to regular function expressions.
Anatomy of two basic arrow function expressions
const basicArrow = () => {
return "The most basic of basic arrow functions";
}
basicArrow();
// prints: "The most basic of basic arrow functions"
const basicArrow2 = oneParam => `Single line with ${oneParam} is also valid`;
basicArrow2("only one param");
// prints: "Single line with only on param is also valid"
Both of the examples above basicArrow()
and basicArrow2()
are valid examples of an arrow function. As with all function expressions, both anonymous functions are implicitly named by the variables assigned to them.
What’s different though is that the function
keyword is unnecessary. Instead it’s replaced by a set of parentheses ()
if there are no required parameters, the name of the single parameter required by basicArrow2()
, which is oneParam
(no parentheses necessary in this case), or, for any other number of parameters, you could do (paramOne, paramTwo, paramThree, ...)
.
Similarly, the first function has a normal return
statement inside the body of the function surrounded by curly braces {}
but, if the statement is super simple, and you can fit the return on a single line, the actual return
and curly braces can also be omitted, like in basicArrow2()
. This is concise body syntax with an implied return statement.
Arrow function traits
While arrow functions are easy to identify at first glance, they actually have some traits which are odd and specific to them, and which developers need to keep in mind.
In addition to their concise syntax, arrow functions lack the this, arguments, super, or new.target keywords.
These facts also lend themselves to one of the arrow function’s biggest drawbacks: they are ill suited as methods and cannot be used as constructors. I’ll get to that in more detail soon.
1. Shorter function syntax
The first, and biggest improvement, in my opinion, to regular function expressions is the shorter, more concise syntax that arrow functions offer.
Here’s the exact same function written as a traditional function expression, then written again as an arrow function expression.
Traditional function expression
var elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
elements.map(function(element) {
return element.length;
});
// this statement returns the array: [8, 6, 7, 9]
New arrow function expression
var elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
elements.map((element) => element.length);
// this statement still returns the same array: [8, 6, 7, 9]
Look at those and tell me the second function isn’t easier to read and follow what the code is doing.
That by itself, is my biggest reason to want to use arrow functions whenever possible. It’s just so much cleaner and clearer.
2. Hoisting still doesn't apply
Just like with traditional function expressions, hoisting still does not apply to arrow functions.
No hoisting, only TypeErrors
console.log(fish());
// prints: TypeError: fish is not a function
const fish = () => ["perch", "salmon", "trout", "bass"];
console.log(fish());
// prints: ["perch", "salmon", "trout", "bass"]
If you try to call the fish()
function before it’s declared in the code, a TypeError
will be thrown.
The solution, as before, is to either declare the function as a function declaration so it gets hoisted to the top of the scope, or wait until after the function expression to call the code.
3. No separate "this"
Before arrow functions, every new function defined its own this
value based on how the function was called:
- A new object in the case of a constructor,
undefined
in strict mode function calls,- The base object if the function was called as an "object method".
An arrow function, on the other hand, does not have its own this
.
The this
value of the enclosing lexical scope is used. Arrow functions follow the normal variable lookup rules of starting at the current scope level and searching all the way to the highest level looking for the variable. So while searching for this
which is not present in the current scope, an arrow function ends up finding the this object from its enclosing scope.
See the example below to see the difference.
this
scope, according to function declarations
function Person() {
// The Person() constructor defines `this` as an instance of itself.
this.age = 0;
setInterval(function growUp() {
// In non-strict mode, the growUp() function defines `this`
// as the global object (because it's where growUp() is executed.),
// which is different from the `this`
// defined by the Person() constructor.
this.age++;
}, 1000);
}
var p = new Person();
this
scope, according to arrow functions
function Person(){
this.age = 0;
setInterval(() => {
// `this` properly refers to the Person object
this.age++;
}, 1000);
}
var p = new Person();
4. No binding of arguments
In addition to not having access to this
, arrow functions do not have their own arguments
object.
Since you may not have heard of them, arguments
is an Array
-like object inside a function that contains the values passed to that function. I say Array
-like because this arguments
has a length property and indexing but it lacks Array’s built-in methods like .forEach()
and .map()
.
Thus, in this example, arguments
is simply a reference to the arguments of the enclosing scope.
arguments
with arrow functions
var arguments = [1, 2, 3];
var arr = () => arguments[0];
arr();
// prints: 1
function foo(n) {
var f = () => arguments[0] + n;
// foo's implicit arguments binding. arguments[0] is n
return f();
}
foo(3);
// prints: 6
In most cases, using rest parameters is a good alternative to using an arguments
object.
Rest parameters instead of arguments
with arrow functions
function foo(n) {
var f = (...args) => args[0] + n;
return f(10);
}
foo(1);
// prints: 11
The rest parameters, which I’ll discuss in another blog post later in this series, is the ES6-recommended way of accessing and manipulating arguments
inside of arrow functions.
5. No use of "new" as a constructor
Ok, last arrow function trait to know about: arrow functions cannot be used as constructors and will throw a TypeError
when used with the new
keyword.
var Foo = () => {};
var foo = new Foo();
// prints: TypeError: Foo is not a constructor
And that’s it. That’s generally what you need to know about arrow functions. Simple! 😅
Conclusion
JavaScript is an incredibly powerful programming language, and its popularity only continues to increase (if the yearly developer surveys are to be believed). With so many developers using it though, there’s bound to be some misconceptions and knowledge gaps, especially with the growing widespread adoption of ES6 into everyday use.
My aim is to shed light on some of the JavaScript and ES6 syntax you use everyday, but may never have fully grasped the nuances of why it works the way it works.
Functions of all kinds are a staple of JavaScript, be they traditional function declarations or function expressions, or the newer, more concise ES6 arrow functions. Knowing when (and how to) effectively take advantage of the benefits of each type of function will definitely make writing JS easier.
Check back in a few weeks, I’ll be writing about JavaScript, ES6 or something else related to web development.
Thanks for reading, I hope you feel better equipped to incorporate arrow functions into your JavaScript applications. Please share this with your friends if you found it helpful!
References & Further Resources
Want to be notified first when I publish new content? Subscribe to my newsletter.