black rectangular sign with red edges hanging on door, with the word "closed"

Understanding Javascript: Closures

6 min

Here we are, continuing our journey of understanding Javascript, and as already stated in the title of the post, we are going to talk about closures. I know this topic is already a little known, especially because it is not exclusive to Javascript, but it is very important for a good understanding of the language. Furthermore, we will understand what execution contexts, scopes and hoisting are.

But before continuing, I want to thank you very much for the many positive feedbacks I received on the previous post, I hope this one is even better!

(This post is part of the “Understanding Javascript” series, to read from the beginning, go to the first post)

How does Javascript find things?

In a program we use several variables to store values that we work with, and we reference them throughout the program. For example:

let x = 20;
function addTen() {
x += 10;
}

Javascript identifies that a variable was referenced, and does the work of looking for its current value, adding 10, and finally assigning the resulting value to that same variable. Now consider this following case:

function declareX() {
let x = 20;
}
x += 10;

What happens when running this code? We received a nice error:

Uncaught ReferenceError: x is not defined

Hmm... when the variable is declared inside the function, Javascript does not detect it outside the function. Yeah, there are rules for how we can reference and access variables. We can call these rules…

Scope

A scope is like an invisible box that contains variables and the rules about how they can be accessed. In Javascript, there are three types of scopes:

See this example:

function exampleFunction() {
if (true) {
let blockVar = 'block scoped';
var functionVar = 'function scoped';
}

console.log(functionVar); // 'function scoped'
console.log(blockVar); // ReferenceError: blockVar is not defined
}

exampleFunction();

In the code above we have 3 scopes. The exampleFunction function is in the global scope, the blockVar variable is in the block scope created by if, and the functionVar variable, as it is not linked to block scopes, is in the exampleFunction function scope.

It's all connected, man...

To be clear, each function has access not only to its own scope, but also to the scope of the functions that contain it, in addition to the global scope. This forms a scope chain, allowing functions to access variables not just in their immediate scope, but in all original scopes.

Going up?

Before going into more detail about scopes, I want to talk about another concept, which despite not being directly linked to closures is also important, called hoisting. In Javascript, variable and function declarations are, in a way, "elevated" in scope.

For example:

console.log(myVar); // undefined
var myVar = 42;
console.log(myVar); // 42

Surprisingly, even before the declaration, the variable myVar is recognized (as it does not give an error) due to hoisting. However, the assignment only occurs where the declaration is in the code. This behavior is even more noticeable with function declarations (not expressions), which can be invoked before being declared.

Variables with let or const have different behavior. Hoisting also happens, but when trying to access them before the declaration, you will enter the temporal dead zone, and therefore you receive a reference error.

console.log(myLetVar);  // ReferenceError: myLetVar is not defined
let myLetVar = 42;

Therefore, added to the fact that var does not enter block scopes, it is often said that it is good practice not to declare variables with it, but only with let or const. However, by being aware of the behaviors, you can use them to your advantage.

What happens inside a function

When starting to execute a Javascript program, the engine creates the global execution context, which is the initial environment for code interpretation. In this context, global variables and functions are declared and can be accessed from anywhere in the program.

When a function is called, a local execution context is generated for that specific function. This context stores local variables and function parameters, ensuring data isolation and integrity.

As functions are called, new local execution contexts are created and stacked on the call stack (we will talk a little more about this concept in another post), being deleted as the functions complete execution, thus removing the variables and functions stored with it.

But if that happens, then how does the code below work?

function createCounter() {
let counter = 0;
return function increment() {
counter++;
console.log(counter)
}
}
const counter = createCounter();
counter() // 1
counter() // 2

When executing the createCounter function, its execution context is closed, thus removing the counter variable from memory. But we return another function, and somehow that function keeps the value of counter! This is where the powerful concept of closure comes in.

You're arrested!

Where you define your functions determines what data they have access to when you call them, and this is what we call lexical scope. Therefore, when declaring a function, if other functions are declared internally, the scopes of the child functions have access to the “parent scope”.

Furthermore, the child function maintains a link to the execution context of the function to which it was defined, thus keeping a persistent reference to the data within the lexical scope! And this is what is commonly called closure, or if you prefer a “more technical” term: Persistent Lexical Scope Referenced Data.

This is particularly useful for creating functions that encapsulate specific behaviors, such as a module. Or, before ES6, create functions that simulate classes with private and public properties. For example:

function Pokemon(name) {
this.name = name;
this.level = 1;
const EV = 0;
}

const pikachu = new Pokemon("Pikachu");
pikachu.name; // 'Pikachu'
pikachu.level; // 1
pikachu.EV; // undefined

In the next post we will cover the behavior of this and new in detail, so don't worry if you didn't understand exactly everything that happened in the code above, but basically the pikachu variable stored the value of this, and as EV was declared through const, it became as private property.

There's more?

Yes, we could go into more detail about some things we talked about, such as the process in which Javascript identifies scopes, which is a compilation step (yes, in case anyone doesn't know, JS is a compiled language) to help with execution performance . Or the phases of the execution contexts and everything that happens in them, etc.

But for that, I leave the reference materials I used again, in case you want to go deeper. Once again, thank you for being here, I hope you enjoyed it and see you in the next post!

Sources: