Javascript Closures
Lately I've been trying to invest more into my Javascript knowledge. I see myself working with Javascript for the foreseeable future, so I believe diving into more advanced topics will make me a better engineer and stand out more.
The latest topic I've covered is Javascript Closures. I'll admit that I had heard the name, but never felt the urge to find out what it actually was because everyone said it was "advanced JS".
I believe that explaining a concept to an audience has a very powerful impact on the communicator. It forces the communicator to clarify their own understanding of the subject, identify gaps in knowledge, and organize information in a way that others can easily grasp.
Without further due, let's dive into closures.
Getting things straight
Ok, so before diving into closures, let's see some Javascript code so that we lay a common ground.
Functions
Functions in Javascript can be written in may ways.
function one() {
return 'Hello World';
}
const two = function () {
return 'Hello World';
};
const three = () => {
return 'Hello World';
};
const four = () => 'Hello World';
These are all valid functions:
- The function
one
is declared in the traditional way, using thefunction
keyword and given an identifier. - The function
two
is an anonymous function, which doesn't have an identifier. Instead, we assign it to a variable. - The function
three
is an arrow fuction, which is a special type of anonymous function which doesn't require thefunction
keyword. - The function
four
is an arrow function with an implicit return statement
When declaring a variable, we assign an identifier to a value. In Javascript, functions are just like variables, where an identifier is assigned to the code of the function (i.e., the function declaration). This code is follows the same scoping rules and is stored in the execution context of the Javascript runtime just like regular variables.
This allows us to write code like this:
function outer() {
function inner() {
return 'Hello World';
}
return inner;
}
const one = outer();
const two = one(); // Outputs "Hello World"
Because function declarations are treated like variable values, they can be returned by other functions. In this example, notice the absence of the parenthises on the return inner;
statement.
By doing this, instead of returing the result of the execution of the function inner
, we are actually returing the function declaration of the function inner
. That's a huge difference! Because
now we can assign another identifier to the code of the function formerly known as inner
, like we do on the const one = outer()
statement, and call that code from outside the place where the function
was initially declarated.
By the way, functions that return functions, like the outer
function above, are called Higher Order Functions (HOC).
Scope
Now that we've got an understanding of how functions work in Javascript, let's talk about scope.
Given this code:
function outer() {
const name = 'John Smith';
function inner1() {
function inner2() {
return name;
}
return inner2();
}
return inner1();
}
console.log(outer()); // Outputs "John Smith"
Javascript is still able to find the variable name
even though its lexical scope (the scope where it was declared - in this case its the scope of the outer
function) is not the same as where it is used.
The variable name
is searched recursively until found. The scope chain determines the sequence of steps the runtime has to go through to find the lexical scope of the name
variable.
To the set of variables a function has available in its execution context, through which it can find their lexical scope through the scope chain, it's called variable environment (VE).
An interesting example
Now that we've got an understanding of how functions work in Javascript, let's look into this example.
function functionGenerator() {
let count = 0;
function addCount() {
count = count + 1;
return count;
}
return addCount;
}
const counter = functionGenerator();
console.log(counter());
console.log(counter());
Before reading any further, write down what you think the output of this program will be.
Like in the section before, we have a function, functionGenerator
, that returns another function, addCount
.
However, this time the addCount
function accesses a variable declared in the outer function, modifies it, and then returns it.
Then, the addCount
function is returned, assigned to a variable, and called.
At a first glance, you would believe that this would throw an error, right?
Right?
Well, it does not throw an error. In fact, the output of the program is 1, and 2. Let's find out why.
The "backpack"
Let's see what happens by executing the code line by line as if we were the Javascript interpreter.
- A
function
keyword is detected. The identifierfunctionGenerator
is assigned to the function declaration - The
counter
variable is assigned to the output of the execution of thefunctionGenerator
function - A new execution context is started to evaluate the value of the output of the
functionGenerator
function - The variable
count
is assigned the value 0 - A
function
keyword is detected. The identifieraddCount
is assigned to the function declaration. This happens inside thefunctionGenerator
execution context created on step 3 - The function
functionGenerator
returns the function declaration of the functionaddCount
. Notice that the functionaddCount
was not called yet - Returning to the global execution context, we know know that the value of the variable
counter
is the function declaration of the formerly knownaddCount
function - The formerly known
addCount
function is called - A new execution context is started to evaluate the value of the output of the formerly known
addCount
function - We see
count = count + 1
. Since no variable with the namecount
was declared on this execution context, we check our backpack and find a variablecount
with the value 0. We increment it by one - Again, since no variable with the name
count
was declared on this execution context, we check our backpack and find a variablecount
with the value 1. We return its value - The console logs 1
- The formerly known
addCount
function is called again - A new execution context is started to evaluate the value of the output of the formerly known
addCount
function - We see
count = count + 1
. Since no variable with the namecount
was declared on this execution context, we check our backpack and find a variablecount
with the value 1. We increment it by one - Again, since no variable with the name
count
was declared on this execution context, we check our backpack and find a variablecount
with the value 2. We return its value - The console logs 2
I'm sure you're wondering what the backpack is. Let me break it down for you.
When the addCount
function was returned by the functionGenerator
, its function declaration was not the only thing returned. In fact, the references to the variables the addCount
function uses were also returned.
These references are hidden in a private, not publicly accessible store that can only be accessed from within the addCount
function declaration. This store is called [[Environment]]
, and represents whan we called earlier the variable environment.
The consequence of this is that functions can now have private memory and store state, simmilarly to what classes can do.
We call this concept closures.
Practical uses of closures
There are lots of scenarios where closures can be used:
- Memoization - imagine a function that computes the nth prime which is called multiple times. It is useful to memoize previous execution results to improve the execution speed.
- Lock features - supose you are developing a game, and you want to make sure that the win function is only called once. This is possible with closures! Just set a boolean on the outer function, and the inner function check if the boolean was already set to true. If so, return an error message.
- Prevent polluting global state - I'm sure you've already came accross a scenario where you had to store state that should be available globally. Using closures you can prevent polluting the global state, and instead store the state in a private store, not publicly accessible
Closures are also used by Javascript itself, namely in iterators, generators, and promises.