Post cover

JavaScript Closure: The Beginner's Friendly Guide

Updated March 27, 2023

The callbacks, event handlers, higher-order functions can access outer scope variables thanks to the closure. The closure concept is important in functional programming and is often asked during the JavaScript coding interview.

While being used everywhere, closures are difficult to grasp. If you haven't had your "Aha!" moment in understanding closures, then this post is for you.

I'll start with the fundamental terms: scope and lexical scope. Then, after grasping the basics, you'll need just one step to finally understand closures.

Before starting, I suggest you resist the urge to skip the scope and lexical scope sections. These concepts are crucial to closures, and if you get them well, the idea of closure becomes self-evident.

Before I go on, let me recommend something to you.

The path to becoming good at JavaScript isn't easy... but fortunately with a good teacher you can shortcut.

Take "Modern JavaScript From The Beginning 2.0" course by Brad Traversy to become proficient in JavaScript in just a few weeks. Use the coupon code DMITRI and get your 20% discount!

1. The scope

When you define a variable, you want it to exist within some boundaries. E.g. a result variable makes sense to exist within a calculate() function, as an internal detail. Outside of the calculate(), the result variable is useless.

The accessibility of variables is managed by scope. You are free to access the variable defined within its scope. But outside of that scope, the variable is inaccessible.

In JavaScript, a scope is created by a function or a code block.

Let's see how the scope affects the availability of a variable count. This variable belongs to the scope created by function foo():


function foo() {
// The function scope
let count = 0;
console.log(count); // logs 0
}
foo();
console.log(count); // ReferenceError: count is not defined

Open the demo.

count is freely accessed within the scope of foo().

However, outside of the foo() scope, count is inaccessible. If you try to access count from outside anyways, JavaScript throws ReferenceError: count is not defined.

If you've defined a variable inside of a function or code block, then you can use this variable only within that function or code block. The above example demonstrates this behavior.

The JavaScript scope

Now, let's see a general formulation:

The scope is a space policy that rules the accessibility of variables.

An immediate property arises — the scope isolates variables. That's great because different scopes can have variables with the same name.

You can reuse common variables names (count, index, current, value, etc) in different scopes without collisions.

foo() and bar() function scopes have their own, but same named, variables count:


function foo() {
// "foo" function scope
let count = 0;
console.log(count); // logs 0
}
function bar() {
// "bar" function scope
let count = 1;
console.log(count); // logs 1
}
foo();
bar();

Open the demo.

count variables from foo() and bar() function scopes do not collide.

2. Scopes nesting

Let's play a bit more with scopes, and nest one scope into another. For example, the function innerFunc() is nested inside an outer function outerFunc().

How would the 2 function scopes interact with each other? Can I access the variable outerVar of outerFunc() from within innerFunc() scope?

Let's try that in the example:


function outerFunc() {
// the outer scope
let outerVar = 'I am outside!';
function innerFunc() {
// the inner scope
console.log(outerVar); // => logs "I am outside!"
}
innerFunc();
}
outerFunc();

Open the demo.

Indeed, outerVar variable is accessible inside innerFunc() scope. The variables of the outer scope are accessible inside the inner scope.

The JavaScript scopes can be nested

Now you know 2 interesting things:

  • Scopes can be nested
  • The variables of the outer scope are accessible inside the inner scope

3. The lexical scope

How does JavaScript understand that outerVar inside innerFunc() corresponds to the variable outerVar of outerFunc()?

JavaScript implements a scoping mechanism named lexical scoping (or static scoping). Lexical scoping means that the accessibility of variables is determined by the position of the variables inside the nested scopes.

Simpler, the lexical scoping means that inside the inner scope you can access variables of outer scopes.

It's called lexical (or static) because the engine determines (at lexing time) the nesting of scopes just by looking at the JavaScript source code, without executing it.

The distilled idea of the lexical scope:

The lexical scope consists of outer scopes determined statically.

For example:


const myGlobal = 0;
function func() {
const myVar = 1;
console.log(myGlobal); // logs "0"
function innerOfFunc() {
const myInnerVar = 2;
console.log(myVar, myGlobal); // logs "1 0"
function innerOfInnerOfFunc() {
console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0"
}
innerOfInnerOfFunc();
}
innerOfFunc();
}
func();

Open the demo.

The lexical scope of innerOfInnerOfFunc() consits of scopes of innerOfFunc(), func() and global scope (the outermost scope). Within innerOfInnerOfFunc() you can access the lexical scope variables myInnerVar, myVar and myGlobal.

The lexical scope of innerFunc() consists of func() and global scope. Within innerOfFunc() you can access the lexical scope variables myVar and myGlobal.

Finally, the lexical scope of func() consists of only the global scope. Within func() you can access the lexical scope variable myGlobal.

4. The closure

Ok, the lexical scope allows to access the variables statically of the outer scopes. There's just one step until the closure!

Let's take a look again at the outerFunc() and innerFunc() example:


function outerFunc() {
let outerVar = 'I am outside!';
function innerFunc() {
console.log(outerVar); // => logs "I am outside!"
}
innerFunc();
}
outerFunc();

Open the demo.

Inside the innerFunc() scope, the variable outerVar is accessed from the lexical scope. That's known already.

Note that innerFunc() invocation happens inside its lexical scope (the scope of outerFunc()).

Let's make a change: innerFunc() to be invoked outside of its lexical scope: in a function exec(). Would innerFunc() still be able to access outerVar?

Let's make the adjustments to the code snippet:


function outerFunc() {
let outerVar = 'I am outside!';
function innerFunc() {
console.log(outerVar); // => logs "I am outside!"
}
return innerFunc;
}
function exec() {
const myInnerFunc = outerFunc();
myInnerFunc();
}
exec();

Open the demo.

Now innerFunc() is executed outside of its lexical scope, but exactly in the scope of exec() function. And what's important:

innerFunc() still has access to outerVar from its lexical scope, even being executed outside of its lexical scope.

In other words, innerFunc() closes over (a.k.a. captures, remembers) the variable outerVar from its lexical scope.

In other words, innerFunc() is a closure because it closes over the variable outerVar from its lexical scope.

The JavaScript closure

You've made the final step to understanding what a closure is:

The closure is a function that accesses its lexical scope even executed outside of its lexical scope.

Simpler, the closure is a function that remembers the variables from the place where it is defined, regardless of where it is executed later.

A rule of thumb to identify a closure: if inside a function you see an alien variable (not defined inside that function), most likely that function is a closure because the alien variable is captured.

In the previous code snippet, outerVar is an alien variable inside the closure innerFunc() captured from outerFunc() scope.

Let's continue with examples that demonstrate why the closure is useful.

5. Closure examples

5.1 Event handler

Let's display how many times a button is clicked:


let countClicked = 0;
myButton.addEventListener('click', function handleClick() {
countClicked++;
myText.innerText = `You clicked ${countClicked} times`;
});

Open the demo and click the button. The text updates to show the number of clicks.

When the button is clicked, handleClick() is executed somewhere inside of the DOM code. The execution happens far from the place of the definition.

But being a closure, handleClick() captures countClicked from the lexical scope and updates it when a click happens. Even more, myText is captured too.

5.2 Callbacks

Capturing variables from the lexical scope is useful in callbacks.

A setTimeout() callback:


const message = 'Hello, World!';
setTimeout(function callback() {
console.log(message); // logs "Hello, World!"
}, 1000);

The callback() is a closure because it captures the variable message.

An iterator function for forEach():


let countEven = 0;
const items = [1, 5, 100, 10];
items.forEach(function iterator(number) {
if (number % 2 === 0) {
countEven++;
}
});
countEven; // => 2

Open the demo.

The iterator is a closure because it captures countEven variable.

5.3 Functional programming

Currying happens when a function returns another function until the arguments are fully supplied.

For example:


function multiply(a) {
return function executeMultiply(b) {
return a * b;
}
}
const double = multiply(2);
double(3); // => 6
double(5); // => 10
const triple = multiply(3);
triple(4); // => 12

Open the demo.

multiply is a curried function that returns another function.

Currying, an important concept of functional programming, is also possible thanks to closures.

executeMultiply(b) is a closure that captures a from its lexical scope. When the closure is invoked, the captured variable a and the parameter b are used to calculate a * b.

5.4 Encapsulation

Another good application that I enjoy is the ability to implement the encapsulation of a module.

Suppose you have a task to implement stack data structure: you can only push or pop items to the stack.

The regular JavaScript array provides both array.push() and array.pop() methods.

Here's a simple implementation of a stack:


function Stack() {
const items = []
return items;
}
const stack = Stack();
stack.push(3)
stack.push(2)
stack.push(1)
console.log(stack.pop()); // logs 1
stack.length = 0; // erases the stack
console.log(stack.pop()); // logs undefined (broken!)

Open the demo.

Yes, the push and pop operations are supported. But there's a problem: the entire array object is exported, and you can easily erase the stack using stack.length = 0, or do any other operations that are not permitted normally on a stack.

Let's make the Stack implementation encapsulated: allow only push or pop operations to be possible. That's exactly where the closure can help:


function Stack() {
const items = [];
return {
push(item) {
items.push(item);
},
pop() {
return items.pop();
}
}
}
const stack = Stack();
stack.push(3)
stack.push(2)
stack.push(1)
console.log(stack.pop()); // logs 1
stack.length = 0; // Does nothing!
console.log(stack.pop()); // logs 2 (works!)

Open the demo.

This time push() and pop() methods are closures that close over items array. What's interesting is that items variable is now private: it is accessible only inside Stack scope, but not outside.

Calling stack.length has no effect: items is not accessible outside Stack scope.

That was an example of another wonderful benefit of closures: you can use them to create true encapsulation with private variables.

6. Conclusion

The scope rules the accessibility of variables. There can be a function or a block scope.

The lexical scope allows a function scope to access statically the variables from the outer scopes.

Finally, a closure is a function that captures variables from its lexical scope. In simple words, the closure remembers the variables from the place where it is defined, no matter where it is executed.

Closures allow event handlers, callbacks to capture variables. They're used in functional programming. Moreover, you could be asked how closures work during a Frontend job interview.

What about a challenge? 7 Interview Questions on JavaScript Closures. Can You Answer Them?

Do you have any closure questions? Ask me in a comment below!

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸