JavaScript Essentials: Often Overlooked Elements of One of The Most Popular Web Development Languages

Omar Verduzco
SSENSE-TECH
Published in
8 min readDec 17, 2021

Part 3 of the 3-part SSENSE-TECH series on JavaScript essentials. Also read part 1 which covers the usage of the keyword this, and part 2 which covers asynchronous execution.

In this third and final article of the series, I will cover closures. Closures allow for a key principle in object-oriented programming: encapsulation. However, in order to understand how closures work, we first need to understand what scope is and how declaring and accessing a variable works in JavaScript.

SCOPE

The scope of a variable is the block of code within which that variable is accessible. Most of us have heard about how global variables are a bad idea, and how we can pollute the global space. As it turns out, scope is the reason why global variables are accessible at any point in the script, and even in different modules. We could end up having very weird bugs if we are not careful. We can consider avoiding global variables as a good practice.

JavaScript engines perform a compilation step, and the scope definition happens at compile time. Getting too deep into the compilation process is not in the scope of this article, but we need to keep in mind a few of its details from a high-level point of view. First, we need to remember what the lexical environment is.

LEXICAL ENVIRONMENT

For every block of code { … } there is an associated lexical environment, which keeps the reference to all the variables accessible to that code. You can imagine it as an object that contains a dictionary of variables and their respective values. The code being executed will ask its lexical environment for the value of any variable it needs. A lexical environment also keeps a reference to the immediate outer lexical environment (i.e. the one containing it) so that, if a variable can’t be found in a specific lexical environment, it will be searched in the outer ones sequentially until it’s found or until the end of the chain is reached, in which case an error will be thrown. The process of searching in every outer lexical scope before throwing is known as scope chain.

In the example above, the variable nameJohn is declared and found inside the function’s lexical environment.

In this second example, namePeter is not declared within the function, thus it is searched for in the outer lexical environment, in this case, the global scope.

VARIABLE DECLARATION AND HOISTING

There are three different ways to declare a variable in JavaScript: with the keyword var, with the keyword let, and with the keyword const. One of the main differentiators in these keywords is how their declaration and value assignment are performed in a process called hoisting.

Hoisting is performed at compile time. During this step, all the variable declarations and assignments are split and only declarations are moved to the top of their corresponding block of code. This allows access to those variables in different ways as described below.

Declaring a variable using var moves the declaration of the variable to the top of the function containing it, or to the top of the script if everything is at the global scope level. This allows for behaviour like this:

Note that var allows access to a variable even before its declaration in the written code. This is because the hoisting step reads through the code and moves all declarations to the top. As mentioned above, specifically for var, the declaration is moved to the top of the function that contains it, and the default value of undefined is assigned to it, which will be overwritten when the code execution reaches the line where an actual value is assigned.

Declaring a variable using let moves the declaration of the variable to the top of its containing block of code, i.e. the brackets that enclose it. There is a catch with let, which is that the variable does not get a default initialization value like var and an error will be thrown if it is accessed before an actual value is assigned.

Declaring a variable using const will have the same hoisting rules as let; however, as we can see below, a const cannot be reassigned a different value:

Keep in mind that while reassigning an object is not possible when using const, mutating it is still possible:

FUNCTION SCOPE

Functions are hoisted too, but they are fully available before their declaration, meaning that they are not assigned a default undefined value. You can call functions before they are declared and the execution will behave the same way as if you called them afterwards. See this example:

When a function is called, a new lexical environment is created specifically for that function call. Any other calls to the same function will create different lexical environments. This helps keep a reference to the values of variables and parameters for a specific invocation. See the example below:

In this case, the same function is called twice with a different argument value. Each call keeps its own lexical environment and both have the global scope as the outer lexical environment, allowing each call to have access to globalName. It is essential to keep this top of mind in order to understand how closures work.

In a previous article in this series, we talked about how functions are first class citizens, meaning that you can treat them as any other object, and that they can be values returned from other functions. Take a look at this example:

Functions can be factories that build other functions, but the interesting part begins when we understand what happens with the lexical environment of the function that is created.

CLOSURES

As previously described, every lexical environment has a pointer to an outer lexical environment or a pointer to null if it is the outermost one. In the case of a function that is returned by another function, its own lexical environment defines the variables created inside of it, but also points to the lexical environment of the function that closes over it.

It would be intuitive to think that after the enclosing function finishes executing, any variable inside of it would be cleaned up by the garbage collector — in JavaScript anything that is not pointed at by a reference is cleaned up — but because of how the lexical environment works, the inner variables of the enclosing function remain referenced by the instances of the function returned. Let’s take a look at this classic example:

We can see here that the function makeAdder is run twice, with different parameters. Each execution of makeAdder creates a separate lexical environment with the baseAdderValue parameter value set to something specific to the call. The first instance of the created functions will receive the numberToAddToBase argument and will look for what baseAdderValue means. It will traverse the scope chain and find baseAdderValue in the immediate outer lexical environment with a value of 10. For the second instance of the adder function, it will look for baseAdderValue and just like in the first scenario, will find it in the immediate outer lexical environment but this time with a value of 2.

In other words, we are able to hide access to variables and only handle them via exposed functions. This is useful to achieve a fundamental concept in object-oriented programming: encapsulation. Closures allow mimicking of the behaviour of private properties or methods, which JavaScript previously didn’t offer.

THE MODULE PATTERN

A module is a collection of related data and functions, characterized by a division between hidden private details and public accessible details, the latter is also known as “public API”. Modules imply a stateful access control, which basically means keeping an internal state and exposing only what is needed (Least Exposure Principle). The following example illustrates how we can turn a simple JavaScript object with properties and methods into a module with a public API that exposes only what is necessary to be public.

This object has one property, the internal state products, which keeps a record of a list of products. It also has one method: findProduct. The object does what it’s intended to do, but the problem is that it exposes its internal state. Therefore, anyone interacting with it can change the internal state and encapsulation is not achieved. Now let’s see how closures help us create a module that controls the access to its internal state.

With this change, the internal state products is accessible only internally in the module, and there is a single method exposed publicly: findProduct. This is a considerable improvement because now other objects that interact with the product finder won’t be able to access its internal state, and won’t know the process of accessing all the products so that they can finally find on.

FINAL THOUGHTS

Although nowadays there are alternatives to achieve proper encapsulation and keeping the least exposure principle, closures remain a very important topic in the language. Throughout this series of SSENSE-TECH articles we examined how JavaScript is a very flexible language that allows for great freedom while developing applications; this can be a good and a bad thing at the same time. Even though we can get up and running quite rapidly, we can also create poor quality code at the same rate. At SSENSE, we opt for the TypeScript alternative, but if you are working with legacy code or want to stick to JavaScript, it’s recommended to keep closures in mind as a way to control access to your objects.

I hope this series of articles was useful and helped to better understand some of the quirks of the language that, albeit basic, are not so straightforward and require some time to master.

ADDITIONAL RESOURCES AND RELATED LINKS

Editorial reviews by Liela Touré & Pablo Martinez

Want to work with us? Click here to see all open positions at SSENSE!

--

--