Closures are one of the most powerful JavaScript features, but they can be a little daunting at first. Having a solid understanding of closures paves the way for understanding topics like higher-order functions and currying.
We’re going to address a few concepts that help illustrate the principles of closures, higher-order functions, and currying.
Functions in JavaScript are first-class citizens, which means that:
// functions can be assigned to variables const morningGreetings = (name) => { console.log(`Good morning ${name}`); } const eveningGreeting = function (name) { console.log(`Good evening ${name}`); } // functions can be passed as arguments to other functions const todaysGreeting = (morningGreetings, eveningGreeting) => { morningGreetings('Barack') console.log(`Thanks for all you have done during the day`); eveningGreeting('Barack'); } // functions can return other functions function myCounter () { let count = 0 return function () { return ++count; } } const noOfTimes = myCounter(); console.log(noOfTimes()); // 1
The feature we’ll look closely at allows functions to return functions. The feature’s closure depends upon the unique characteristics of JavaScript.
In JavaScript, functions have the ability to reference a variable that isn’t defined in the function but is available within an enclosing function or the global scope.
Consider the following example:
const iamglobal = 'available throughout the programme'; function funky() { const iamlocal = 'local to the function scope funky'; } console.log(iamglobal);// available throughout the programme console.log(iamlocal); // iamlocal is not defined
As you can see, we’re unable to access the variable iamlocal
outside of the scope of the function funky
. This is because the variable is only kept “alive” while the funky is active.
Once the function has been invoked, references to any variables declared within its scope are removed and the memory is handed back to the computer for use.
However, there’s a way we can have access to the variable declared within a function even after the function has been invoked.
This is where closures come in.
A closure is a reference to a variable declared in the scope of another function that is kept alive by returning a new function from the invocation of the existing function.
Let’s take a look at an example:
function outerScope() { const outside = 'i am outside'; function innerScope() { const inside = 'i am inside'; console.log('innerScope ➡️', outside); console.log('innerScope ➡️',inside); } console.log('outerScope ➡️', outside); innerScope(); } outerScope(); // outerScope ➡️ i am outside // innerScope ➡️ i am outside // innerScope ➡️ i am inside
It’s possible to access the value of the variable outside
from function innerScope
. The concept of closures hinges on this capability.
From the example above, it’s possible for us to return the function innerScope
rather than call it within the outerScope
, since this is close to a real world scenario.
Let’s modify the example above to reflect this change:
function outerScope() { const outside = 'i am outside'; function innerScope() { const inside = 'i am inside'; console.log('innerScope ➡', outside); console.log('innerScope ➡',inside); } return innerScope } const inner = outerScope(); inner(); // outerScope ➡️ i am outside // innerScope ➡️ i am outside
This resembles the example above, which illustrates how functions have the ability to return functions.
Let’s take this a step further and look at more of a real-world example:
function closure(a) { return function trapB (b) { return function trapC(c) { return c * a + b; } } } const oneEight = closure(1.8); const thirtyTwo = oneEight(32); const degreeToFahrenheit = thirtyTwo(30); console.log(degreeToFahrenheit); // 86
It can be useful to think of each function declaration as a circle in which each enclosing circle has access to the variables declared in the preceding circle:
In this case, trapC has access to variables a, b and c
, while trapB has access to variable a and b
, and finally the closure has access only to a
.
Higher-order functions are functions that accept another function as an argument, return another function as a result, or both.
So far, we’ve been using higher-order functions as seen in our closure
, outerScope
,todaysGreeting
, and myCounter
examples.
Closures are integral to higher-order functions.
One of the core benefits of higher-order functions is that they allow us to customize the way we call our functions.
Consider the illustration below:
const multiply = (a , b) => { return a * b; } console.log(multiply(2,3)) // 6
If we’re only interested in getting all the multiples of 2 throughout the entire program, you can repeat 2 as one of the arguments throughout our program:
multiply(2,1) // 2 multiply(2,2) // 4 multiply(2,3) // 6
While this works, it introduces a lot of repetition into our code and it violates the DRY (Don’t repeat yourself) principle.
You could also argue that we can hardcode the value of 2 into our function definition. Well, that’s true, but it’ll make our function less reusable.
Let’s redefine the function to use higher-order functions so we can see the benefits and flexibility it offers when calling the function:
const multiply = (a) => { return (b) => { return a * b; } }
Having defined the above function that way, we can create customize function calls as follows:
const multiplyByTwo = multiply(2); console.log(multiplyByTwo(3)) // 6 const multiplyByThree = multiply(3); console.log(multiplyByThree(6)); // 18
We can create customize functions that have practical use and also save us the hassle of repeating ourselves.
Currying is a process that involves the partial application of functions.
A function is said to be curried when all the arguments needed for its invocation have not been supplied. In this case, it will return another function that retains the already-supplied arguments and expect the remaining omitted argument to be supplied before invoking the function.
The function is only invoked when all arguments have been supplied. Otherwise, a new function is returned that retains existing arguments and accepts new arguments as well.
When you curry a function, you call it as f(a)(b)(c)(d)
rather than f(a, b, c , d)
. By extension, all curried functions are higher-order functions but not all higher-order functions are curried.
The bottom line here is that currying allows us to turn a single function into a series of functions.
Let’s consider the following example:
function sum (a, b) { return a + b; } console.log(sum(4,5)) // 9
We can go ahead and curry this function so we have the flexibility to call it partially when all arguments aren’t supplied.
function curriedSum (x,y) { if (y === undefined) { return function(z) { return x + z } } else { return x + y; } } console.log(curriedSum(4, 5)) // 9 console.log(curriedSum(4)(5)) // 9
We don’t have to write another curried implementation of our function every time we need it in order to call it partially. Instead, we can use a general curry function and pass our original function as an argument to it.
Here’s how:
function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } }; }
Let’s use an example to illustrate how this works.
function mean (a , b, c) { return (a + b + c) / 3 } const curriedMean = curry(mean); console.log(curriedMean(1,2,3)) console.log(curriedMean(1,2)(3)) console.log(curriedMean(1)(2)(3))
As you can see, these concepts build upon one another since closures are used extensively in higher-order functions, and higher-order functions are similar to curried functions.
Having a solid understanding of the above concepts gives us insight into how popular JavaScript libraries implement a few functions, e.g. the connect function used by React-Redux.
connect(mapState)(MyComponent)
Currying
Currying is an advanced technique of working with functions. It’s used not only in JavaScript, but in other languages as well. Currying is a transformation of functions that translates a function from callable as f(a, b, c) into callable as f(a)(b)(c). Currying doesn’t call a function. It just transforms it.
JavaScript: Novice to Ninja, 2nd Edition | SitePoint Premium
As comprehensive as it can get. In 600+ pages you’ll go from JavaScript Novice to Ninja. Covering everything from arrays, logic and loops, to functions, objects, DOM, events, testing and debugging, Ajax and more. It’s everything you need to build with JavaScript.
Tracking down the cause of a production JavaScript exception or error is time consuming and frustrating. If you’re interested in monitoring JavaScript errors and seeing how they affect users, try LogRocket.https://logrocket.com/signup/
LogRocket is like a DVR for web apps, recording literally everything that happens on your site.LogRocket enables you to aggregate and report on errors to see how frequent they occur and how much of your user base they affect. You can easily replay specific user sessions where an error took place to see what a user did that led to the bug.
LogRocket instruments your app to record requests/responses with headers + bodies along with contextual information about the user to get a full picture of an issue. It also records the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Enhance your JavaScript error monitoring capabilities – Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.