Arrow Functions: JavaScript ES6 Feature Series (Pt 2)

Paige Niedringhaus - Jun 26 - - Dev Community

Old, beat up chalkboard with scribbled math on it

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 syntax

The most basic arrow function syntax.

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" 
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

If you’d like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .