DEV Community

Martin Himmel
Martin Himmel

Posted on

JavaScript (ES5) Functions - Part 2

This was originally posted on my site at https://martyhimmel.me on January 10, 2017. Like a number of others on dev.to, I've decided to move my technical blog posts to this site.

Last time, we looked at the basics of functions and how to create them. In this tutorial, we'll focus more on using functions and look at recursion, closures, and functions as "first-class citizens" or "higher order functions" (both terms reference the same idea).

Recursion

A recursive function is a function that calls itself repeatedly until some condition is met. You can think of it like a self-looping function.

For the first example, we're going to use a factorial. If you're not familiar with factorials, it's a mathematical term that takes a number and muliplies every number (starting with 1) up to the given number. For example, factorial 5 (written as 5!) is the result of 1 * 2 * 3 * 4 * 5, so 5! = 120. Using that as our base, here's a function that handles it with a standard for loop.

function factorial(num) {
  if (num < 0) {
    return -1;
  }
  if (num === 0) {
    return 1;
  }
  var total = 1;
  for (var i = 1; i <= num; i++) {
    total *= i;
  }
  return total;
}
factorial(5); // 120
Enter fullscreen mode Exit fullscreen mode

Now let's convert this to a recursive function.

function factorial(num) {
  if (num < 0) {
    return -1;
  }
  if (num === 0) {
    return 1;
  }
  return factorial(num - 1) * num;
}
factorial(5); // 120
Enter fullscreen mode Exit fullscreen mode

As you can see, this simplifies the code. Recursive functions have their limits in JavaScript though. Every time you call a function, it gets added to the stack, which takes up memory. The bigger the stack, the more memory being used. If the stack gets too big, the app (or the browser) can hang and/or crash.

Let's look at the Fibonacci sequence as an example. If you're not familiar with it, the Fibonacci sequence is a series of numbers that adds the previous two values. Starting with one, the first few numbers are 1, 1, 2, 3, 5, 8, 13, 21, and so on. 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, and on it goes. The mathematical formula for that is Fn = Fn-1 + Fn-2 - the nature of this formula lends itself really well to a recursive function.

function fibonacci(num) {
  if (num < 2) {
    return num;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
}
fibonacci(8); // 21 - the eighth number in the series
Enter fullscreen mode Exit fullscreen mode

Here's the tricky part - depending on your system, this can hang the browser at a relatively low number. On my laptop, there's a momentary pause at fibonacci(30) while it calculates, about a 2 second wait at fibonacci(40), and it hangs at around fibonacci(55). The reason is the way this recursive function is set up, it makes 2 more function calls for every one value, which calls the function two more times to calculate those values, and so on. Here's a graphical representation of that.

Chart of fibonacci function calls

As you can see, it quickly (and exponentially) builds up the stack. As shown in the diagram, with just a base number of 5, there are 15 function calls. At base number 10, there are 177 function calls. You can see how this gets out of control really fast.

The way to get around that is through memoization - a process of storing known or previously calculated values and passing that information. This results in far fewer function calls, which means the stack isn't anywhere near as large, and the performance is greatly improved. Here's a memoized version of the fibonacci function.

function fibonacciMemoized(num, values) {
  // First call to this function, values will be undefined since the "values" argument isn't passed
  if (typeof values === 'undefined') {
    values = {};
  }

  if (num < 2) {
    return num;
  }

  // Calculate values if needed and stores them in the "values" object
  if (!values.hasOwnProperty(num - 1)) {
    values[num - 1] = fibonacciMemoized(num - 1, values);
  }
  if (!values.hasOwnProperty(num - 2)) {
    values[num - 2] = fibonacciMemoized(num - 2, values);
  }

  return values[num - 1] + values[num - 2];
}
fibonacciMemoized(8); // 21
Enter fullscreen mode Exit fullscreen mode

In this case, any time a value is calculated, it gets stored in the values object, which is passed with each call. In the non-memoized version, the function calls are being made even if the same value was calculated in another branch. In the memoized version, once a value is calculated, it never has to be calculated again. The values object is checked for the number's value and, if it exists, it uses it instead of calling the function again. The branching looks more like this now:

Chart of memoized fibonacci function calls

In the non-memoized version, there was an exponential increase in the number of function calls as the base number grew larger. In the memoized version, as the base number grows larger, the number of function calls is only one more than the base number, resulting in a significantly smaller stack and an exponential increase in performance compared to the previous version. On my laptop, calling fibonacciMemoized(1000) returns instantaneous results, whereas the non-memoized version completely crashed around 55.

Closures

The simplest definition of a closure is a function within a function, but that definition doesn't capture the power of closures. In a closure, the inner function has access to the outer function's variables and parameters. Here's an example.

function displayWelcomeMessage(firstName, lastName) {
  function fullName() {
    return firstName + ' ' + lastName;
  }
  return 'Welcome back, ' + fullName() + '!';
}
console.log(displayWelcomeMessage('John', 'Smith')); // Welcome back, John Smith!
Enter fullscreen mode Exit fullscreen mode

The inner function takes the parameters from the outer function and concatenates them, then passes it back to the outer function. The outer function then creates a welcome message with the results of the inner function, then returns the full welcome message.

One of the benefits of closures is that it creates a private scope. In the above example, if you try to call fullName() anywhere outside of the displayWelcomeMessage function, you'll be met with an Uncaught ReferenceError: fullName is not defined error. fullName() is only available inside displayWelcomeMessage.

Closures and Immediately-Invoked Function Expressions

One of the common ways of using closures is with an Immediately-Invoked Function Expression (IIFE). An IIFE is a function that runs as soon as it's created. Here's a comparison of a standard function and an IIFE.

// Standard function
function foo() {
  // do something
}
foo(); // Need to call the function to use it

// Immediately Invoked Function Expression
(function() {
    // do something right now
})(); // anonymous IIFE - the code inside is executed immediately (no need to call it later)
Enter fullscreen mode Exit fullscreen mode

Often times, you will find entire scripts wrapped in an IIFE. Doing so prevents the global scope from being polluted with variables and functions. It essentially creates a privately scoped module. This is actually the basis for several design patterns in JavaScript as well, such as the module and revealing module patterns.

Higher Order Functions

JavaScript functions are referred to as "higher order" or "first class" functions (they're the same thing). What this means is that functions can be used in a similar way as objects - you can pass a function as an argument, return a function from another function, etc. Event listeners are dependent on accepting functions as arguments.

function buttonClickListener() {
  console.log('You clicked a button!');
}
document.getElementById('myButton').addEventListener('click', buttonClickListener);
Enter fullscreen mode Exit fullscreen mode

This example attaches a click event to the button with an id attribute of "myButton" by passing the name of the function (buttonClickListener) as an argument. You may have noticed this is a bit different than calling the function. When you call a function, you include the parentheses (e.g. myFunction()). When you pass a function as an argument, you don't include the parentheses. If you do, it will execute the function immediately rather than pass it as an argument.

You can also use anonymous functions as arguments. This example has the same functionality of the previous, but is done with an anonymous function instead.

document.getElementById('myButton').addEventListener('click', function() {
  console.log('You clicked a button!');
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)