Learning Journey Node.js: thoughts on arrow functions

Learning Journey Node.js: thoughts on arrow functions

Up until this point in my career as a JavaScript/Frontend engineer, I've tried to just follow along with best practices as expressed via popular articles, library/package documentation, and any coursework I find myself leveling up with. I haven't typically given much forethought to "why" something is considered a best practice.

Until now... :D.

I'm currently going through a Node.js course (The Complete Node.js Developer Course), authored by Andrew Mead and Rob Percival (great work on the course, BTW, gents! :) ). In my career, thus far, as a Frontend engineer, I've mostly stuck to frontend frameworks and technologies that are mainstream: Angular 1.x, then Angular 2+, followed by React/TypeScript. In that time, I've used function syntax:

function getSquaredNumber(x) {
  return x * x;
}

console.log(getSquaredNumber(2)) // 4

...as well as arrow-function syntax:

const getSquaredNumber = (x) => {
    return x * x;
};

console.log(getSquaredNumber(2)) // 4

...as well as the ES6 method definition syntax:

const numberUtils = {
  getSquaredNumber(x) {
    return x * x;
  },
};

console.log(numberUtils.getSquaredNumber(2)) // 4

It would be easy to believe that each of these functions simply flavors of the same base function syntax. After all, they all produce the same results. Yet, there are some meaningful ways in which they are different.

THIS Context

If you're working with object-oriented programming syntax, you might find yourself using this keyword to access a property on that object. As an example:

const person = {
  name: 'Nathan',
  getName: function() {
    console.log(this.name);
  },
};

person.getName(); // "Nathan"

Invoking person.getName() results in logging the string "Nathan". This is because this is within the context of the person object; therefore this.name refers to the value at the person.name property.

Ok... so what's the big deal?

While the getName property method has the context of the this keyword, if we change it to an arrow function:

const person = {
  name: 'Nathan',
  getName: () => {
    console.log(this.name);
  },
};

person.getName(); // undefined

Now invoking person.getName() results in logging undefined. But why? Aren't the method syntaxes synonymous? Well, if you're smart (not like me), you'd discover that the arrow function inherits its parents' context. In this case, no name property of its parent context has been defined. This is what that'd look like:

this.name = 'Johnny'

const person = {
  name: 'Nathan',
  getName: () => {
    console.log(this.name);
  },
};

person.getName(); // "Johnny"

Now invoking person.getName() results in logging "Johnny". That's because the arrow function is inheriting the this from its parent context (which is the block of JavaScript above).

Why use arrow functions at all then?

A lot of the time arrow functions are useful when you want to maintain the context of the parent context.

Let's say for example that we are trying to iterate through a list of people, and log out their names:

const person = {
  name: 'Nathan',
  getName() {
    return this.name;
  },
  friends: [
    'Bobby', 'John', 'Kevin', 'Joey',
  ],
  listFriends() {
    // log out the friend list
  },
};

We could try to use the function syntax:

const person = {
  name: 'Nathan',
  getName() {
    return this.name;
  },
  friends: [
    'Bobby', 'John', 'Kevin', 'Joey',
  ],
  listFriends() {
    this.friends.map(function(friend){
      console.log(`${this.name} is friends with ${friend}`);
    })
  },
};

person.listFriends();
// " is friends with Bobby"
// " is friends with John"
// " is friends with Kevin"
// " is friends with Joey"

Using the function syntax results in 4 console logs of " is friends with [friend name]". Why is the person.name not being listed? This is due to the function() {} having its own this context. The variable friend is scoped to the function since it's the callback value of the map function, but this.name is in the context of the parent listFriends function.

Solution...

If we replace the function(friend) {} with an arrow function, the arrow function will inherit the this context from the listFriends ES6 method definition syntax, and bob's your uncle.

const person = {
  name: 'Nathan',
  getName() {
    return this.name;
  },
  friends: [
    'Bobby', 'John', 'Kevin', 'Joey',
  ],
  listFriends() {
    this.friends.map((friend) => {
      console.log(`${this.name} is friends with ${friend}`);
    })
  },
};

person.listFriends();
// "Nathan is friends with Bobby"
// "Nathan is friends with John"
// "Nathan is friends with Kevin"
// "Nathan is friends with Joey"

Why does this matter?

It used to be that when you were working with promises and callbacks you had to bind context down through the callback functions. It was hell.

With the arrow function, however, you can treat the nested functions as being part of their parent, but having code isolation from other arrow functions since they have their own scope, but inherit from their parent. This is incredibly useful for reducing bleed effects across your code as you use functional patterns.

Hope this helps in your journey. :)