JavaScript Closures: What They Are and Why You Need Them
Closures might be confusing at first, but it's important to understand them well to use them effectively. You might require a few re-reads to get things cleared.
Before we begin, it's important to understand that JavaScript uses lexical scoping. This means that functions use the variable scope that was in effect when they were defined, not the scope when they are called.
In order to implement lexical scoping, the internal state of a JavaScript function must remember not only the code of the function but also the scope in which the function definition appears.
This combination of a function code and a scope in which the function’s variables are resolved is called a closure.
Technically, all JavaScript functions are closures, but often it doesn't really matter since most functions are called from the same scope they were defined in.
Closures become interesting when they are invoked from a different scope than the one they were defined in. This happens most commonly when a nested function object is returned from the function within which it was defined.
The first step to understanding closures is to review the lexical scoping rules for nested functions. Consider the following code:
let greet = "Hello"; // A global variable function greetUser() { let greet = "Hi"; // A local variable function f() { return greet; // Return the value in greet here } return f(); } greetUser(); // Hi
The greetUser()
function declares a local variable and then defines and invokes a function that returns the value of that variable.
The internal function f()
can access the variables of the outer function.
Let’s change the code just slightly. Can you tell what this code will return?
let greet = "Hello"; // A global variable function greetUser() { let greet = "Hi"; // A local variable function f() { return greet; // Return the value in greet here } return f; } let s = greetUser()(); // What does this return?
In this code, instead of invoking the nested function and returning its result, greetUser()
now just returns the nested function object itself.
What happens when we invoke that nested function (with the second pair of parentheses in the last line of code) outside of the function in which it was defined?
Remember the fundamental rule of lexical scoping: JavaScript functions are executed using the scope they were defined in.
The nested function f()
was defined in a scope where the variable greet
was bound to the value "Hi"
. That binding is still in effect when f
is executed, no matter where it is executed from.
So the last line of the preceding code example returns “Hi”
, not “Hello”
.
This, in a nutshell, is the powerful nature of closures: they capture the local variable (and parameter) of the outer function within which they are defined.
Why Do You Need Them?
Closures capture the local variables of a single function invocation and can use those variables as private states.
To better understand how we use variables as private states, let's consider this example.
let incrementCounter = (function () { // Define and invoke let counter = 0; // Private state of function below return function () { counter = counter + 1; return counter; }; })(); incrementCounter(); // => 1 incrementCounter(); // => 2
In order to understand this code, you have to read it carefully.
The code defines and invokes a function, so it is the return value of the function that is being assigned to incrementCounter
.
Now, if we study the body of the function, we see that its return value is another function. It is this nested function object that gets assigned to incrementCounter
.
The nested function has access to the variables in its scope and can use the counter
variable defined in the outer function.
Once that outer function returns, no other code can see the counter
variable: the inner function has exclusive access to it.
Private variables like counter
need not be exclusive to a single closure: it is perfectly possible for two or more nested functions to be defined within the same outer function and share the same scope.
function incrementCounter() { let counter = 0; return { count: function () { counter = counter + 1; return counter; }, reset: function () { counter = 0; }, }; } let c = counter(); c.count(); // 1 c.reset(); // 0 => reset() and count() methods share state c.count(); // 1
The incrementCounter()
function returns an object. This object has two methods: count()
returns the next integer, and reset()
resets the internal state.
Both count()
and reset()
share the same counter
variable.’
Another important point to keep in mind is that arrow functions inherit the this
value of the function that surrounds them. However, functions defined using the function
keyword do not inherit the this
value in the same way.
Therefore, if you're creating a closure that needs to use the this
value from its surrounding function, it's advisable to use an arrow function within the closure before returning it. Alternatively, you can assign the outer this
value to a variable that your closure will inherit.
Learn Higher-Order Functions
Write code that is easier to understand and maintain.
You've heard about higher-order functions, but every explanation you've come across is either too technical or too vague. You're searching for something clear and practical, with examples that will help you practice and truly master the concept.
Higher-order functions are key to writing better code, essential for your projects, and expected in interviews. But getting started can be tough.
But what if you could? What if you had access to easy-to-understand content that not only explains higher-order functions but also provides hands-on practice? You’d finally feel confident using them in your projects, knowing exactly what they do and how to leverage them effectively.
It’s true, finding the right resources to learn higher-order functions can be challenging… but it doesn’t have to be.
Get my guide, where you’ll:
- Understand why higher-order functions are worth learning and how they can transform your approach to writing JavaScript.
- Gain clarity as I break down higher-order functions into simple, everyday language.
- Master three important higher-order functions:
map
,filter
, andreduce
, with plenty of examples to solidify your understanding. - Apply your knowledge with real-world examples that demonstrate how these concepts work together, so you can use them easily in your projects or write better code in your interviews.
- Get practice exercises to help reinforce what you’ve learned and boost your confidence in understanding the concepts.