Eda Eren

March 9, 2022
  • JavaScript

Lexical Environment and Closures in JavaScript

When it feels like this is the worst of times and the age of foolishness, and you're almost certainly sure that there are darker times ahead, you might as well have a certain desire to understand how things work on a deeper level beneath the surface.

One of those things that are beneath the surface of JavaScript is the concept of Lexical Environment. If you're familiar with closures, it is something that helps you internalize what is really going on.

We can think of the Lexical Environment as an object that every function, code block, even the whole script itself, has. It not only contains the local variables and their values, but also has a reference to an outer lexical environment.

When you create a variable, let's say, something like this:

let book = 'Harry Potter and the Prisoner of Azkaban';
let book = 'Harry Potter and the Prisoner of Azkaban';

Think of the book as a property of the Lexical Environment, with the value 'Harry Potter and the Prisoner of Azkaban'. Since it is inside the global Lexical Environment now, the outer reference is null. Maybe another way to think about this is that the global Lexical Environment is the environment of the whole script, and it has not any reference to anything outer than itself.

How the global Lexical Environment behaves is different for variables and declared functions. Let's try to understand what we mean by that.

The global Lexical Environment is filled with all the variables, but initially, the variables are "uninitialized" — which means that the engine knows about them, but they cannot be referenced until they've been declared. So, let's say this is our script for now:

let book; // (1)
book = 'Harry Potter and the Prisoner of Azkaban'; // (2)
book = 'Harry Potter and the Goblet of Fire'; // (3)
let book; // (1)
book = 'Harry Potter and the Prisoner of Azkaban'; // (2)
book = 'Harry Potter and the Goblet of Fire'; // (3)

What happens when the execution starts, is that the (global) Lexical Environment knows about the variable book, but it is uninitialized. On line (1), book is now undefined. On line (2), book is assigned a value, 'Harry Potter and the Prisoner of Azkaban'. On (3), the value of book is changed to 'Harry Potter and the Goblet of Fire'.

However, we said that the case is different for function declarations. It also shines light on the "hoisting" aspect of JavaScript. Let's take a look at it.

When a function is declared (we're not using a function expression), it is instantly initialized so that it is ready to be used. That's why it does not matter if we declare the function after we use them — that's why something like this works:

console.log(add(30, 3)); // 33

function add(num, num2) {
return num + num2;
}
console.log(add(30, 3)); // 33

function add(num, num2) {
return num + num2;
}

When we say that JavaScript "hoists" a function, what actually happens is this: declared functions are instantly initialized when the Lexical Environment is created. But, let's look at this now:

let broomstick = 'Firebolt';

function summonItem(spell) {
return `${spell} ${broomstick}!`;
}

console.log(summonItem('Accio')); // Accio Firebolt!
let broomstick = 'Firebolt';

function summonItem(spell) {
return `${spell} ${broomstick}!`;
}

console.log(summonItem('Accio')); // Accio Firebolt!

When the execution of the above code starts, the Lexical Environment knows both broomstick and summonItem; however, broomstick is uninitialized at this stage while summonItem is initialized and ready to use. To visualize, think of the Lexical Environment as an object with properties like below:

{
broomstick: <uninitialized>,
summonItem: function
}
{
broomstick: <uninitialized>,
summonItem: function
}

Also, of course, its outer references null because this is the global Lexical Environment.

When a function starts running, a new Lexical Environment is created for it. So, when we call summonItem (inside the console.log), the Lexical Environment of that call only stores spell having the value 'Accio'. And, it also has its outer referencing the global Lexical Environment itself, which stores broomstick and summonItem, with its own outer referencing null. The Lexical Environment of our function call (summonItem('Accio'))—the Inner Lexical Environment— references the outer one, the global Lexical Environment. That is, spell is found locally, but to reach broomstick, the outer reference is followed, and it is found there.

So, it is true to say that:

When the code wants to access a variable – the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.

Now, it's time to catch our breath.

It may be a lot at first, but, that's learning 💁🏻.

This time, consider this one:

function powersOfTwo() {
let start = 2;
let count = 0;
return function() {
return start ** count++;
}
}

let twoToThePower = powersOfTwo();

console.log(twoToThePower()); // 1 (2 ** 0)
console.log(twoToThePower()); // 2 (2 ** 1)
console.log(twoToThePower()); // 4 (2 ** 2)
console.log(twoToThePower()); // 8 (2 ** 3)
console.log(twoToThePower()); // 16 (2 ** 4)
console.log(twoToThePower()); // 32 (2 ** 5)
function powersOfTwo() {
let start = 2;
let count = 0;
return function() {
return start ** count++;
}
}

let twoToThePower = powersOfTwo();

console.log(twoToThePower()); // 1 (2 ** 0)
console.log(twoToThePower()); // 2 (2 ** 1)
console.log(twoToThePower()); // 4 (2 ** 2)
console.log(twoToThePower()); // 8 (2 ** 3)
console.log(twoToThePower()); // 16 (2 ** 4)
console.log(twoToThePower()); // 32 (2 ** 5)

When the powersOfTwo is called, a Lexical Environment is created for it. It now has start and count, and outer referencing the global Lexical Environment which has powersOfTwo and twoToThePower, as well as its own outer referencing null.

When we call twoToThePower inside console.log, what happens is — you guessed it, a new Lexical Environment is created. Since start and count are not inside this local Lexical Environment, it follows the outer reference (which is the Lexical Environment of powersOfTwo). When it updates the count, it is updated inside the Lexical Environment of powersOfTwo. Another way to put it:

A variable is updated in the Lexical Environment where it lives.

Again, start and count lives inside the Lexical Environment of powersOfTwo. When we update count, it is updated there, not inside the Lexical Environment of the returned function which we bind to twoToThePower.

In the first call of twoToThePower, start is 2 and count is 0. In the second call, start is still 2, but count is updated and is now 1. And, it keeps being updated inside the Lexical Environment where it lives (powersOfTwo) as long as we call twoToThePower.

So, twoToThePower has the "power" to access and modify the variables inside of a Lexical Environment that its outer references.

This is what closures are about, a function that has access to its outer scope.

Here comes the enlightenment: Then, are not all functions closures in JavaScript?

I guess the answer is mostly yes, with an exception.

If you remember the summonItem example, it also accesses a variable (broomstick) from its outer scope, so based on the definition, we can say that it is theoretically a closure. Though, it might be better if we don't confuse ourselves a lot because when you look up closures, most basic examples you see would be similar in spirit to powersOfTwo. It is nevertheless a nice thing to internalize, as it was our goal all along — to see how things work beneath the surface. It is an abstract surface of course, but good to dive into.

References