Learn

Welcome to the CodeSmith Learning Center! Here you'll find content focusing on advanced JavaScript and programming concepts.

Object-oriented programming was the most prominent programming paradigm for 25-30 years, but functional programming has taken the lead over the last 5-7 years. As Zoolander's Mugatu would say, "It's so hot right now."

The reason for its rise is clear -- as applications grow in complexity, functional programming gives developers the ability to deliver intuitive, stateless, side-effect free code, greatly streamlining their development process and improving their applications.

Callbacks and higher-order functions are at the heart of functional programming, and they're an essential part of a JavaScript developer's toolbox. Let's explore these concepts together from the ground up.

Definitions

If you type “callback” into Google search, you'll see the first definition as “an invitation to return for a second audition or interview.” That’s definitely not the kind of callback we’re discussing here!

In JavaScript, a callback is a function that is passed as an argument to another function. If that doesn’t make sense yet, don’t worry -- we’ll break it down soon.

And what about higher-order functions? A Google search reveals an accurate description: “a function that takes at least one function as input and/or returns a function as output.”

Hmm...

Let’s see a code example to clarify:

function add2(num) {
  return num + 2;
}

function callingNumber(callback, num) {
  return callback(num);
}

callingNumber(add2, 5); // returns 7

So we have two function definitions, and we're executing callingNumber with add2 as input. One of these functions is the callback, and one of them is the higher-order function. Can you guess which one is which?

Spoiler alert: the callback is add2, and the higher-order function is callingNumber!

Why is add2 the callback? It satisfies these two conditions:

  1. It is used as input in another function call.
  2. It is called inside of the other function.

Why is callingNumber the higher-order function? It satisfies these two conditions:

  1. It takes a function as an argument.
  2. It calls its input function when it is called. Hence, the term "callback" as one function call leads to another.

So basically, higher-order functions have callbacks as inputs and call those callbacks somewhere in their definition.

To clarify, although we may call a function a “callback” or a “higher-order function”, they are both just normal JavaScript functions. Nothing special about them except the terminology we use to describe them.

When Do You Use This Stuff

As fundamental building blocks in JavaScript, callbacks and higher-order functions are used in almost every web application. They solve a general set of code duplication problems.

Let’s start thinking about those problems. We’ll build up our understanding through a series of questions that highlight how we can write cleaner, more powerful code with callbacks and higher-order functions.

The Questions Begin

Let's say you are tasked with manipulating an array of numbers, such as stock prices. You begin by testing your skills on a small array to see if you can simply add 10 to each integer.

You might write some code that looks like this:

const array = [1, 2, 3];
const output = [];

for (let i = 0; i < array.length; i++) {
  const updated = array[i] + 10;
  output.push(updated);
}

console.log(output); // prints [11, 12, 13]

If that were the only addition operation you needed to perform in your application, then this code is absolutely fine. However, you realize you need to add other values, too. How would you write new code to add other values, like 20, to each number?

Welp… looks like we’ll have to copy/paste the entire code block just to make one change:

const array = [1, 2, 3];
const output = [];

for (let i = 0; i < array.length; i++) {
  const updated = array[i] + 20;
  output.push(updated);
}

console.log(output); // prints [21, 22, 23]

It feels dirty to repeat so much code for such a small change. And it’s inefficient because we have to store all this duplicated code in memory.

You may have heard of the DRY principle (Don’t Repeat Yourself). This code breaks that principle. In colloquial terms, it’s not DRY code.

[Video: Solution to DRY code]

For Every Problem, There Is A Solution

How can we solve this repetition problem? In other words, how do we abstract away the code that doesn’t change? Think about this deeply, then keep reading.

***Stop here if you are just starting to think deeply***

***If you haven’t thought deeply, you shouldn’t be reading this. Think deeply then come back!***

***Start here when you’re done thinking deeply***

Time for the answer -- Any time we notice we are writing code that repeats except for one small part, we turn it into a function! The function body will contain the repetitive code block, and its parameter will replace the one thing we want to change. For example, if we want to change the number we’re adding, just replace all instances of that number with the parameter num:

const array = [1, 2, 3];

function update(num) {
  const output = [];
  for (let i = 0; i < array.length; i++) {
    const updated = array[i] + num;
    output.push(updated);
  }

  return output;
}

update(10) // returns [11, 12, 13]
update(20) // returns [21, 22, 23]

How great is that! Instead of having to copy/paste the same code block over and over and over every time we want to change the number added, we just write one function. Now we can call that function with any number as input and get the appropriate output.

In other words, when we create our functions with parameters like this, we have no idea which number will be added in the future. Think about how cool that is... we are delivering a function that will calculate any possible addition you could throw at it… and all we did was abstract away one number!

This is incredibly useful if you need to frequently transform array elements using addition in your applications.

One Step Further

Thinking about our stock prices example, if all we needed to do was add values to the prices, then we could clap our hands and go home -- we would have all the functionality we need. Hooray!

But what if we wanted to perform other operations, like subtraction or more complex manipulation? Functions cannot take operators (like + and -) as parameters, so that's one limiting factor. But more importantly, we can't deliver custom functionality through simple value substitution.

If only we could use other functions as parameters to deliver custom functionality inside our function... oh wait, we can!

First-Class Citizens To The Rescue

In JavaScript, functions are first-class citizens. This means that functions can be passed as input to other functions (as well as returned from other functions, modified, and assigned to variables). This gives developers the ability to use functions as parameters, which we refer to as callbacks.

So how could we perform more complex calculations in our stock prices example? Well, if a number parameter allows us to use any number (like 10 or 20), then a function parameter allows us to use any functionality (like adding, subtracting, or more complex manipulation)!

That’s hard to visualize without a concrete example. So, here’s a concrete example. Notice how the updated variable has changed and we can apply three different functions to the array quite easily:

const array = [1, 2, 3];

function update(callback) {
  const output = [];
  for (let i = 0; i < array.length; i++) {
    const updated = callback(array[i]);
    output.push(updated);
  }

  return output;
}

// Callback functions
function add10(num) {
  return num + 10;
}

function multiplyBy20(num) {
  return num * 20;
}

function stringify(num) {
  return num.toString();
}

// Call update with each callback function
update(add10) // returns [11, 12, 13]
update(multiplyBy20) // returns [20, 40, 60]
update(stringify) // returns [‘1’, ‘2’, ‘3’]

Compared to our previous example, we still changed the same variable, updated. Instead of using the ‘+’ operator and a number parameter, we replaced that code with a function call to the array element. This small change now allows us to use any function, referred to as a callback, when we run the update function!

This makes update a higher-order function because it now takes a callback as input. In fact, I’ve been hiding a secret from you this whole time… this update function is essentially the built-in map function in JavaScript!

Given a callback, map will output a new array where each element is the output of a call to the callback. It provides us a powerful tool to transform arrays, and we control the transformation through callbacks.

[Video: Convert code into higher-order function]

Wrapping Up

Let’s summarize what we’ve discussed in this article.

  • Callbacks are functions that are passed as input to other functions.
  • Higher-order functions take at least one function as input (and/or return a function, although we haven’t dug into that aspect of them in this article. You can learn more about that in our post on Closures!)
  • Functions give us the power to use the same code without copy/pasting it. We insert parameters where we’d like control over changing values.
  • Higher-order functions provide an additional layer of abstraction, using callbacks as parameters to control functionality instead of simple values. The built-in map function is one example of an incredibly useful higher-order function that transforms arrays with callbacks.

That’s all, folks. Take a whack at the challenges below to see how much you’ve learned and improve your mastery over these crucial programming concepts!

OUTLINE

  1. Execution Context
  2. Scope
  3. What is a closure?
  4. What are closures used for?

Execution Context

The execution context is the environment in which a JavaScript function is being executed. Each execution context has its own variable scope.

Whenever a function is invoked, a new execution context is pushed onto the call stack, and a new variable environment is created along with it. The function’s code runs in the new execution context and has access to the variable environment in the execution context directly below that new execution context (the one in which the function was invoked).

Upon exit of the function, the execution context is popped off of the call stack. Code in one execution context is stopped until the code in the execution context above it finishes running.

Scope

Variable environments are commonly referred to as “scope.” While each execution context is stacked on top of the execution context preceding it, each scope is created inside of the previous scope. Code being executed inside a scope not only has access to the variables in that scope, but also the variables in all the scopes that encase it. Therefore, code that runs in a given execution context has access to all the variables created in the execution contexts below it.

[Video: Creating a new variable environment on invocation of function]

What is a closure?

A closure is a variable environment that has outlived its execution context and remains attached to a function that also has outlived the same execution context.

If multiple functions are created in the same execution context and all of them outlive that execution context, those functions each have their own closure, and each of these closures will access the same variables in memory. Therefore, mutation of those variables by one of those functions will affect the other functions in the manner that they will be accessing the same (mutated) variables.

[Video: A function returning another function, attaching a closure to the returned function]

What are closures used for?

Closures give us the ability to gain some benefits normally associated with object-oriented programming, namely data privacy and data encapsulation (thus name “closure”). When the structure of the encapsulated data and the methods used to access/manipulate the data is too simple to justify using actual object-oriented programming, closures provide a convenient solution.

[Video: Using closure to create private variables accessible only by the function created in the same execution context]

Closures can aid in maintaining secure and private access to variables, by creating functions that serve as the only objects with access to these variables. The functionality of the functions themselves serves as a method of whitelisting the ways that those variables can be mutated. Because only the created functions have access to the variables, the procedural steps outlined in the created functions are the only procedures that can operate on the variables in the closure.

Closures are used especially often when functionality is being imported into a file from another file. The code in the imported file is executed, creating a closure for any function(s) exported from the file. This is a common method for JavaScript libraries to be made available for use to a developer’s application and helps developers maintain modularity of their code.

Recursion is one of the most complex and powerful concepts in computer programming. It allows us to solve problems elegantly and succinctly, providing a new tool that shapes the very fabric of our thinking.

But to understand recursion, we must look at code in an entirely new way. We'll start by breaking problems down into a collection of repeatable mini-steps, and we'll illuminate some key computer science concepts that allow recursion to work under the hood.

Although recursion has some performance limitations compared to iterative solutions, it does offer us the extreme benefit of making our code clean, readable and intuitive when implemented well.

So let's get rolling!

On Function Calls

We know that functions can perform operations on strings, numbers, arrays and objects. But what happens if we try to execute a function inside another function?

function add2(num) {
  return num + 2;
}

function add2ThenMultiplyBy3(num) {
  const value = add2(num);
  return 3 * value;
}

add2ThenMultiplyBy3(8);

This works and produces the expected output of 30! The reason is that a function is just another data type, and all data types can be manipulated by functions in Javascript.

So if functions can call other functions, what happens if a function tries to call itself? For example:

function callingMyself() {
  callingMyself();
}

This is recursion -- a function calling itself. In our example, however, we did not provide any other code, so callingMyself would invoke callingMyself for eternity! Clearly, we need a way to stop this infinite loop so we can recurse without breaking the computer.

The particular error code you’ll see when running infinite recursive calls is “Maximum call stack size exceeded.” So what is the call stack? And what is this “maximum” value?

The Call Stack

Every computer contains a particular area of memory set aside for function calls. This space, known as the call stack, creates a new execution context for a function’s arguments, local variables and operations every time you call a function.

Each new execution context exists on one frame on the call stack. When a function makes a recursive call (that is, a call to itself), a new execution context enters on TOP of the previous one, adding a frame to the call stack.

We can visualize this with a stack of pancakes, where each pancake is a frame:

Stack of pancakes

Well that looks yummy… How high can the stack go? It depends on the specific Javascript environment (Chrome, Firefox, Node.js, etc.), size of your function and memory your computer has allocated for the call stack -- a good estimate is roughly 10,000. So if you plan to recurse more than 10,000 layers deep, beware that you may see the infamous “Maximum call stack size exceeded” stack overflow message even without an infinite loop!

Using Recursion Responsibly: Base Cases

If we need to end our recursive calls at some point, how do we do that? With base cases!

A base case is a conditional statement that contains a return statement. Assuming we return anything other than another function call, our return statement saves the day and actually stops the recursion! The frames start to pop off one by one until the call stack is empty. We’re saved from the claws of infinite loops!

For example, this stack stops growing after the 10th recursive call:

let counter = 0;

function stopAt10() {
  if (counter === 10) return 'We counted to ten!';
  counter++;
  return stopAt10();
}

stopAt10();
console.log(counter); // prints 10

Here is exactly what happens:

  • We declare a global counter variable then assign it a value of 0.
  • We define a function called stopAt10 then store that definition in global memory.
  • We invoke stopAt10, which creates our first frame on the call stack.
  • We enter that frame’s execution context.
  • We see that counter does not equal 10, so we do not enter the base case.
  • We increment the counter by 1.
  • We make a recursive call, which creates our second frame on the call stack.
  • We enter the second frame’s execution context.
  • Counter is 1, not 10, so we do not enter the base case.
  • We increment the counter by 1.
  • We make another recursive call, which creates our third frame on the call stack.
  • …repeat until the step when counter increments to 10
  • We make a recursive call, which creates our 11th frame on the call stack.
  • We enter the 11th frame’s execution context.
  • Since counter does equal 10, we enter the base case and return our string!
  • Everything now rewinds because we hit return. The 11th frame pops off the call stack and sends its returned value to the 10th frame’s return statement.
  • The 10th frame pops off the call stack and sends its returned value to the 9th frame.
  • … repeat until we arrive at our first frame.
  • The first frame pops off the call stack and sends its returned value to the global execution context where we originally called stopAt10!
  • Our console log will show that counter is 10.

That’s a lot happening in the computer just to count to 10!

To Iterate Or Not To Iterate

Since recursion has so much overhead, we often prefer to use iterative loops for simple operations like counting. Compare our stopAt10 recursive function we used above to the while loop below:

let counter = 0;

while (counter !== 10) {
  counter++;
}

console.log(counter); // prints 10

Here is exactly what happens:

  • We declare a global counter variable then assign it a value of 0.
  • We see that counter is not equal to 10, so we enter the while loop.
  • We increment the counter to 1.
  • We see that counter is still not equal to 10, so we enter the while loop again.
  • We increment the counter to 2.
  • … repeat until counter increments to 10.
  • Counter is 10, so the condition evaluates to false and we exit the while loop.
  • Our console log will show that counter is 10.

Notice how many fewer steps there are! No additional frames on the call stack required. This makes iteration faster than recursion almost 100% of the time in Javascript. Iteration is also safer and provides more flexibility; I could iteratively loop to 100,000 just fine, whereas the recursive solution would break around 10,000 as it exceeds the maximum call stack size.

Performance issues aside, a key takeaway here is that recursion is remarkably similar to a while loop! The code block continues running based on some condition.

Do note a minor difference in their conditional expressions, though: recursion stops when a base case evaluates to true, whereas a while loop stops when its condition evaluates to false.

Play With Recursion

Now that you know the basics, try writing a factorial function with recursion! Factorials take an input and multiply that input by all integers below itself until 1. For example, if the input is 4, then you should return 24, which is 4 * 3 * 2 * 1.

Don’t proceed with the chapter until you’ve built factorial and understand how recursion works at a basic level!

function factorial(num) {

}

console.log('This is a console, so use console.log to print!')


          

Parameters As Storage

In writing factorial, you may have used a global variable similar to the stopAt10 example with counter. This is a perfectly valid way of approaching a base case. However, what if we didn’t want to use any global variables so we don’t pollute the global namespace?

It turns out that parameters in recursion make great storage variables! Since our recursive calls always carry parameters as variables for the next execution context, we can transmit new information to our next call.

Let’s see how that looks with a factorial function where we build up a product parameter (with a default value of 1) that we eventually return:

function factorial(num, product = 1) {
  if (num === 0) return product;
  const nextProduct = product * num;
  const nextNum = num - 1;
  return factorial(nextNum, nextProduct);
}

Notice that we approach our base case where num is 0 by subtracting 1 from num on every recursive call, and we increase our product by multiplying it with num on every call. Thus, we simultaneously build our answer and approach our base case using parameters!

"If we strip away the nextProduct and nextNum variable assignments and insert those values directly as arguments for the next call, the code becomes even more elegant:

function factorial(num, product = 1) {
  if (num === 0) return product;
  return factorial(num - 1, product * num);
}

Remember to return your recursive call so that the product can be returned to the outermost function!

Finally, if we apply ES6 arrow functions and a ternary operator, we get the most elegant form of them all: the one-liner!

const factorial = (num, product = 1) => num === 0 ? product : factorial(num - 1, product * num);

While this may not be the most readable way to write the factorial function, it sure is cool to see it boiled down to just one short line of code!

Recursion: What Is It Good For?

Some consider recursion dangerous because of the possibility of stack overflows. Others consider it poor practice because it is not as performant as iterative solutions for simple cases. However, when algorithms get more complex, recursion offers us dynamic solutions that iterative solutions cannot.

Data structures like trees and graphs lend themselves heavily to recursive solutions. Mathematical algorithms with permutations and combinations are also solved elegantly with recursion.

Sometimes we’ll want to create new execution contexts to track variables instead of keeping all variables in one context and mutating them. This is a key paradigm that powers functional programming, which we will touch on later in this course.

Finally, when building software, tradeoffs are necessary. Recursion is one of those tools in your toolbox that should be kept at the ready, equipped to make your code more readable, elegant and powerful when need be.

Challenge 1

Now it’s time to flex your recursive muscles! Write a function that takes two inputs, a base and an exponent, and returns the expected value of base ^ exponent. For instance, if base is 2 and exponent is 3, then return 8 since 2^3 = 2 * 2 * 2 = 8.

function pow(base, exponent) {

}

console.log('This is a console, so use console.log to print!')


          

Challenge 2

Get the length of an array using recursion without accessing its length property.

function getLength(array) {

}

console.log('This is a console, so use console.log to print!')


          

Challenge 3

Write a function called recurseFuncs that takes an array of functions and a number. Our function recurseFuncs should call the input functions in order with that number, where the result of each function becomes the next function’s input. Use recursion to return the final value after all functions have been used.

For example:

const funcs = [
    (a) => 8 * a - 10,
    (a) => (a - 3) * (a - 3) * (a - 3),
    (a) => a + 4,
    (a) => a % 5
 ];

recurseFuncs(2, funcs); // → 1

As we can see, the first function is called with an input of 2 and returns an output of 6. Then 6 passes into the second function to return 27. Then 27 passes into the third function to return 31. Then 31 passes into the last function to return 1. Since we have used all functions, we return 1 from recurseFuncs.

function recurseFuncs(input, funcArray) {

}

console.log('This is a console, so use console.log to print!')