Difference between arrow functions and regular functions

ST

Seb Toombs

Sep 25 2020 ()

5 min read

The difference between regular functions and arrow functions

ES6 and subsequent ECMAScript releases have featured the arrow function, amongst other things. The arrow function seems to be one of the features that causes much confusion and takes some time to learn.

There are 3 key differences between an arrow function and a regular (named) function.

First, we'll go over what each type of function is, then I'll quickly show you the differences. We're only looking at the differences in terms of how they might affect you day-to-day. For a more in-depth look, Mozilla has a good reference article on arrow functions.

A regular (named) function in javascript looks like this;

function formatName(firstName, lastName) {
  return firstName + " " + lastName;
}

And an arrow function might look like this;

const formatName = (firstName, lastName) => {
  return firstName + " " + lastName;
}

The differences

1. "this"

An arrow function does not have it's own this keyword. Instead, this is bound to the parent this. In the case where the arrow function is executed in the global context (it has no parent function/scope), this will be bound to the global object (window in the browser or globalThis in node).

When is this useful?

This is particularly helpful when you have some function whose this you might need to access from within an inner function. For example consider this pattern. (It's a bit contrived, but it shows how useful an arrow function can be when you need to access parent this).

// Let's create a controller to manage the value of some counter
// when a button is pressed
function ButtonController() {
  // initialise the count value
  // here "this" is the ButtonController instance
  this.countValue = 0;
  
  // create an arrow function to increase out counter
  // within the arrow function "this" refers
  // to the same "this" as above
  const increaseCount = () => {
    this.countValue += 1;
  }
  
  // As an example, this would be how we would have to do
  // the same thing, without arrow functions
  
  // we capture the parent "this" in some variable
  var _this = this;
  function decreaseCount() {
    // _this is now available in the scope of decreaseCount
    _this.countValue -= 1;
  }
  
  return {
    increaseCount,
    decreaseCount,
    get countValue() {
      return this.countValue;
    }
  }
}

const controllerInstance = SomeController();

// And we could bind this method to the on click event of a button
controllerInstance.increaseCount();

In the above example, increaseCount is an arrow function, and can easily access this from the ButtonController instance.

In contrast, decreaseCount is a named function, and to access this from the ButtonController instance, we have to save it to a variable (_this) and then reference it inside.

If you've done much with jQuery back in the day, this pattern might look familiar to you (capturing parent this for reuse inside a closure).

2. Implicit return

One of (in my opinion) the best things about an arrow function is implicit returns.

What is an implicit return? Take the example at the top of the post. If we have a function that is really only a one-liner, it'd be nice not to have to write out the curly brackets and return statement, right?

Well, arrow functions allow us to do that!

// Instead of writing this function like this:
const formatName = (firstName, lastName) => {
  return firstName + " " + lastName;
}

//We can make use of implicit returns instead
const formatName = (firstName, lastName) => firstName + " " + lastName;

If your arrow function is written without the curly brackets, it will implicitly (by default) return whatever comes next.

When is this useful?

Implicit returns from arrow functions becomes especially useful when working with array methods or callbacks.

Let's see some examples;

//Let's make an array of fruit.
const fruitArray = ["Apple", "Banana", "Orange"];

//Now we want an array of fruit but all lower case
//Here we will use Array.map, and make use of the implicit return
//from the arrow function that we pass as the callback
const fruitArrayLowerCase = fruitArray.map(fruit=>fruit.toLowerCase());

//The same thing without an arrow function would look like
const fruitArrayLowerCasae = fruitArray.map(function(fruit) {
  return fruit.toLowerCase();
});


//Imagine we need to chain a couple together:
const fruitArrayFiltered = fruitArray
  .map(fruit=>fruit.toLowerCase())
  .filter(fruit=>fruit.charAt(0) === "a");

Quick note:

If you want to return an object literal from your arrow function with an implicit return, you'll need to wrap it in brackets (parentheses). This is because passing a "curly" bracket to the arrow function looks like a function body, not a return value.

Example;

// The following function returns an object literal with
// the two arguments as properties
const myRegularFuncThatReturnsAnObject = function(a,b) {
  return {a,b};
}

// To do the same with an arrow function, we need to wrap
// the return value in brackets "(" ")"
const myImplicitReturnFunc = (a,b) => ({a,b});

3. Always anonymous

The third major difference between arrow functions and regular functions is that arrow functions are always anonymous. They cannot be named. Whereas a regular function created with the function keyword can be named or anonymous (function myFunc() {... } vs function() { ... }), arrow functions are always anonymous.

Most of the time, this isn't a problem, as function naming is often only important when being passed to an event listener addEventListener('click', myFunc) so it can be removed again or similar.

The one main way that this impacts me is usually to do with hoisting. If you're not sure how hoisting works, the quick version is this;

Named functions are hoisted to the top of the current scope.

What does this mean? It means you can call a named function in your script higher up that where it is declared (within the same scope).

For example

// In this first example, we're using a named function, *before*
// it's declared. This is because hoisting allows us to call
// any named function within the same scope, despite the fact
// that the function isn't declared until the end of the scope
function initMyComponent1() {
  
  // This works
  this.addEventListener('click', handleComponentClick);
  
  // handleComponentClick is a named function and thus
  // it is hoisted to the top of initMyComponent1 scope
  function handleComponentClick(e) {
    // Do something
  }
  
}

// In this second example, we're trying to use an anonymous
// function *before* it's been declared
// This will throw an error!
function initMyComponent2() {
 
  // THIS WILL FAIL!
  this.addEventListener('click', handleComponentClick);
  
  const handleComponentClick = (e) => {
    // Do something
  }
}

This largely only affects how you structure your file, but handy to know!

Limitations

Just like anything, arrow functions are good in moderation & there are some cases where you don't or can't use them!

1. Use as methods or constructors

Arrow functions shouldn't be used as methods. That's not to say you can't use them as methods, but they're not best suited. This example from Mozilla demonstrates why quite nicely. Essentially, as arrow functions don't have their own this, they can't access any other properties from a parent object.

Arrow functions can't be used as constructors, and will throw an error if you try.

2. call, apply & bind

Because arrow functions use this from the scope that they were created in, using call, apply & bind with arrow functions can lead to some unexpected results, again I'm going to defer to Mozilla on that one because, well, it's boring.

When to use an arrow function?

When should you use an arrow function? Good question!

An easy rule of thumb; if you don't need access to the local this within your closure, go with an arrow function.

For example, simple utility functions, pure functions etc, an arrow function will do the trick.

A great use for arrow functions is with setTimeout, setInterval, and addEventListener. These methods will execute regular function callback with the global or window scope. Which can often lead to strange behaviours when the callback is executed.

// This won't work, as the addEventListener callback is
// executed on the global (window) scope
// obj.count is always 1
const obj = {
  count: 1,
  addEventListener: function(el) {
    el.addEventListener('click', function() {
      this.count ++;
    });
  }
}

// This will work as expected, the arrow function is created
// with the obj scope, and obj will be incremented
const obj = {
  count: 1,
  addEventListener: function(el) {
    el.addEventListener('click', ()=>{
      this.count ++;
    });
  }
}

When to use a regular, or a regular named function

When should you use a regular (or a named) function? There are a few cases where you definitely want to use a regular function;

  • You need the function's this or super access
  • An object constructor
  • An object method*
  • If you want to use yield within the function body
  • You want to use call, apply, or bind

*You can use either type of function for an object method, but it will depend on your use case.