Skip to main content

Functions in JavaScript

5 min read

Older Article

This article was published 9 years ago. Some information may be outdated or no longer applicable.

ES2015 gave us a new way to write functions: the arrow function syntax. It’s terser, yes. But it also changes how this behaves, and that matters more than you’d think.

Functions are chunks of your code that perform a specific task. The key thing about them? They’re reusable. You write them once, call them wherever you need them.

Say we’ve got an application that needs to greet people. We wouldn’t want to scatter console.log('Hello [name]') all over the place. So we wrap that logic in a function and call it when needed:

function greet(name) {
  return 'Hello' + name;
}

console.log(greet('Steve')); // Hello Steve
console.log(greet('Ann')); // Hello Ann

Real-world functions tend to be bigger and more involved, of course.

arrow functions

Arrow functions (sometimes called ‘fat arrow’ functions) let you define functions more concisely in ES2015.

You’ve probably written something like this before:

const numbers = [0, 1, 2];
numbers.map(function (number) {
  return console.log(number);
});

We can rewrite that with an arrow function by dropping the function keyword and replacing it with =>. There are some rules around how arguments and the return keyword work (we’ll get to those shortly):

const numbers = [0, 1, 2];
numbers.map((number) => console.log(number));

this

The difference between regular functions and arrow functions isn’t just shorter syntax. Arrow functions bind this lexically to the calling function.

That sentence probably feels a bit confusing (excuse the pun). Let’s look at how this works in regular JavaScript.

Here’s a relatively simple example. We’ve got a JavaScript object that calculates quotients:

const quotient = {
  numbers: [1, 2, 3, 4, 5, 6, 7],
  results: [],
  divideFn: function (divisor) {
    return this.numbers.map(function (divident) {
      if (divident % divisor === 0) {
        return this.results.push(divident);
      }
    });
  },
};

We store a sample dataset in numbers, collect results in results, and use divideFn() to iterate through each number. If the modulo operator (%) returns 0 (meaning the number is divisible), we push it into our results array.

To find numbers divisible by 3, add these two lines:

quotient.divideFn(3);
console.log(quotient.results);

Run this and you’ll hit an error: Cannot read property 'push' of undefined. That’s odd. results is definitely an array, so it should have a push() method.

Start debugging and you’ll spot the culprit: the execution context and the this keyword. Try dropping a console.log(this) just before the second return statement inside the if block.

Whether you run this in Node.js or a browser, you’ll get a different result. But either way, logging this spits out an object that isn’t our quotient object.

Here’s what’s happening. The .map() function iterates through the numbers array. It takes an anonymous callback, and inside that callback we use this. But this is contextually sensitive here. It references the anonymous function, which gets hoisted to the global scope.

There are several workarounds for “this” problem in regular JavaScript (excuse the pun, again). The first is to stash this in another variable:

const quotient = {
  numbers: [1, 2, 3, 4, 5, 6, 7],
  results: [],
  divideFn: function (divisor) {
    let that = this;
    return this.numbers.map(function (divident) {
      if (divident % divisor === 0) {
        console.log(that);
        return that.results.push(divident);
      }
    });
  },
};

Now the error disappears, and that points to our object.

Another common fix is calling .bind() with this as the argument. The .bind() method creates a new function scope and sets this to whatever value you pass in:

const quotient = {
  numbers: [1, 2, 3, 4, 5, 6, 7],
  results: [],
  divideFn: function (divisor) {
    return this.numbers.map(
      function (divident) {
        if (divident % divisor === 0) {
          return this.results.push(divident);
        }
      }.bind(this)
    );
  },
};

These solutions work fine. But with ES2015 arrow functions, you don’t need any of them. Arrow functions keep this lexically bound to the calling function. So our example becomes:

const quotient = {
  numbers: [1, 2, 3, 4, 5, 6, 7],
  results: [],
  divideFn: function (divisor) {
    return this.numbers.map((divident) => {
      if (divident % divisor === 0) {
        return this.results.push(divident);
      }
    });
  },
};

No errors. Correct execution. Expected result.

This is a massive win for JavaScript. Anonymous callbacks show up everywhere in libraries like jQuery, and developers constantly trip over this in those contexts.

Here are the basic syntax rules for arrow functions:

  • One argument? Skip the parentheses
  • Multiple arguments? Use parentheses
  • Single expression? Curly braces are optional (return is implicit)
  • Returning an object? Wrap it in parentheses
// return is implicit
const numbers = [0, 1, 2];
numbers.map((number) => console.log(number));

// curly brackets, use return:
numbers.map((number) => {
  number *= 2;
  return console.log(number);
});

You can also use arrow functions for non-callback functions. Here’s how to return an object using parentheses:

const greet = (name, age) => ({
  name: name,
  age: age,
});

console.log(greet('Steve', 18));