Arrow Functions: Beyond the Shorter Syntax

March 23, 2021

ES6 Arrow functions are often favored because of their shorter syntax, especially when passed as anonymous callbacks. Here are some other details that differentiate them from functions defined with the function keyword.

this in arrow functions

The this keyword behaves differently between Arrow functions and the function keyword. To quote MDN:

[...] Arrow functions establish "this" based on the scope the Arrow function is defined within.

In slightly different terms, an Arrow function will set this lexically (i.e., the location of the function definition) and based on the "parent" scope, rather than resolving this dynamically (i.e., the function invocation) and based on the scope it's called from.

Here's an example:

// global scope

const BankAccount = { // doesn't create a new scope
  amount: 10,

  print: function() {
    /*
      `this` is defined dynamically, i.e. based on the
      function invocation. 
      since we call it with BankAccount.print(), `this`
      will refer to BankAccount.
    */

    console.log('this will refer to BankAccount')
  },

  printWithArrow: () => {
    /*
      `this` is determined lexically and based on the outer scope. 
      Since BankAccount does not create a scope, the outer cope is 
      the global scope. Hence, `this` inside the Arrow function
      will refer to the global scope.
    */

    console.log('this will refer to the window object (global scope)')
  }
}

BankAccount.print()
BankAccount.printWithArrow()

print has this resolved to the BankAccount object because we invoke it with BankAccount.print().

Since Arrow functions do not have their own this, printWithArrow will bind this to the scope it's defined within. In our case, that is the global scope.

Passing functions around

One word is critical: Arrow function will adopt the scope they are defined within.

That means that Arrow functions will bind this automatically to the parent (i.e., the outer scope). After it is bound, this will not change when reassigning them or passing them as callbacks.

I like to think of it roughly like calling .bind(this), only that it automatically does it for you.

class Person {
  constructor() {
    this.name = 'My name'
  }
  
  printWithTimeout() {
    // this refers to Person

    const normalFunction = function() {
      console.log(this.name)
    }
    
    const arrowFunction = () => {
      console.log(this.name)
    }
    
    setTimeout(normalFunction, 500) // this.name will be undefined
    setTimeout(arrowFunction, 500) // this.name is 'My name'
  }
}

const me = new Person()
me.printWithTimeout()

In the example above, a method called printWithTimeout is defined. Within this method, we call setTimeout with functions: one created using the arrow function syntax, while the other uses the function keyword.

The one created with the function keyword will output this.name as undefined. The reason is that the function uses dynamic this binding, meaning it uses the function invocation to resolve this. When normalFunction gets called by setTimeout, the scope will be the global scope. Because this is bound dynamically, normalFunction will not determine this.name as intended.

However, with arrow functions, the behavior is different. Arrow functions will resolve this based on the scope they are defined within. Since we defined the arrow function in Person's scope, it will correctly resolve this.name. Even after reassigning the arrow function, this will still refer to the lexically bound value.

This is why arrow functions are often easier to work with when passing them as callbacks or reassigning them.

When not to use an arrow function

Arrow functions do have their drawbacks because of the way this works and other limitations. Here's what to keep in mind:

  • Using them as methods

    As we saw earlier, using this will not achieve the desired effect when used as methods as it will not resolve to the object itself; hence they are not suited for methods.

  • Using them with apply, call, and bind

    Arrow functions should not be used in conjunction with these methods because of the lexical binding of this.

  • Lexical super, arguments, and new.target

    As with this, arrow functions will determine the bindings and keywords lexically based on the surrounding scope.

  • Being used as a constructor

    Arrow functions cannot be used as a constructor using the new keyword.

  • Accessing their prototype

    .protoype of Arrow functions will resolve to undefined.

  • Using yield

    The yield keyword is not permitted in Arrow function bodies.