A Look at JavaScript Closures
A few weeks ago, I had a technical interview where I was tested on closures. I had studied them during school, but that day, I realized I needed more practice to gain a thorough understanding. After much research and practice, here’s what I learned.
What is a Closure?
In JavaScript, a closure is a feature where a function nested inside of another function retains access to the outer function’s variables, even after the outer function has returned. A closure is also created whenever a new function is created.
Let’s take a look at an example:
let greeting = 'Hi'const outer = () => { let name = 'Vanessa'; const inner = () => {
let nickname = 'Vani';
console.log(`${greeting}! My name is ${name}, but my friends call me ${nickname}.`);
} inner();
}
Above, the inner function has access to the global variable greeting
and the outer function’s variable, name
. This is made possible through lexical scope, which refers to a variable, function, or object’s accessibility based on its physical location in the code. So, because our inner function is nested in the outer function, it has access to the variables defined in:
- its own function scope
- the outer function’s scope
- the global scope
So, when you invoke outer()
, which invokes inner()
, the JavaScript engine prints out:
outer(); // Hi! My name is Vanessa, but my friends call me Vani.
What is the Lexical Environment?
As mentioned above, the inner function has references to its surrounding state (outer function). That surrounding state is known as the lexical environment. In JavaScript, this environment is described as a theoretical object, [[Environment]]
, with two parts:
1. Environment record — an object where local variables (and more) are stored
2. A reference to the outer lexical environment
Again, you can’t access this hidden object, but it’s useful for understanding how closures work. In the function below, each time getLikes()
is called, a new lexical environment object is created and stores the variables for this particular function call. When getLikes()
is executed, we also create the nested anonymous function that returns likes++
, but we don’t increase likes just yet. We are only creating the function, not running it.
const getLikes = () => {let likes = 0; return () => { return likes++; };}
let showMeLikes = getLikes();showMeLikes() // 0
That’s why when we assign getLikes
to showMeLikes
, we have a reference to {likes: 0}
. It’s because all functions remember the lexical environment in which they were created. It’s held in the hidden [[Environment]]
property.
So, the first time we call showMeLikes()
, it returns 0 because it references the lexical environment from function creation time. The next time it’s called, a new lexical environment is created, so showMeLikes
checks its own environment (finds no variables), then checks its outer lexical environment getLikes
, where it finds the variable likes
and increases it.
showMeLikes() // 0showMeLikes() // 1showMeLikes() // 2
Not Erased From Memory
A very important point to note is that usually lexical environments are wiped clean after a function execution. That means that all variables are erased with it. However, in closures, inner functions retain access to their outer lexical environment. That is what makes closures so powerful in JavaScript.
Sources: