Closure is one of the features of the language that many programmers struggle to wrap their heads around. Yet, it is one of the core building stones of JS and a frequent topic that appears in coding interviews. In this article, you will learn all about it.
What happens when you call a function?
To understand closure, we need to fully understand how a JavaScript program works. Our code is interpreted line by line. Variables are stored in the global memory and we have a single execution thread.
When the interpreter reaches a function invocation the function is placed on the call stack and a new execution context is created for the function - it has its local memory and thread of execution. Think of it as a mini-program. When the function returns, either explicitly (reaching a return statement) or implicitly (by default functions return undefined), the function leaves the call stack and its execution context is destroyed.
What the heck is Closure?
Imagine this piece of code.
let name = "John"
function greet() {
const greeting = "Hi"
function printHi() {
console.log(greeting + ' ' + name)
}
printHi()
}
name = "Jane"
greet() // "Hi Jane"
Our inner function printHi has access to the local memory of its parent (greet) and the global memory. Notice, that we actually have access to the "fresh" data that is available during the call, not the declaration. That is how lexical scoping works in JavaScript.
But what would happen if we return a function instead of just calling it within the body of the outer function. Well, here the magic comes. A returned function from another function is not just a simple function definition, it is the definition plus the variables it has access to and needs to run stored in a backpack that comes with it.
What we just described is the mysterious closure. Formally, a closure is a when function remembers its lexical scope (the backpack) even when the function is called outside of that lexical scope.
function creator(num) {
return function() {
num = num * 2
console.log(num)
}
}
const double = creator(5)
double() //10
double() //20
const double2 = creator(7)
double2() // 14
double2() //28
double() // 40
As we can see in the snippet above, whenever we call double it updates the same piece of data (num from its parent function) that is stored in its backpack, which is technically the hidden [[scope]] property the function has.
If you are wondering why is it useful, check the examples below.
Module pattern
The closure allows us to protect or hide certain pieces of information. The backpack is a hidden property so we can't just access it and update it as we would do with a standard object literal. It is also important to mention, that we can return a set of functions stored on an object and they would all be closures.
In the snippet below we are taking advantage of the so-called IIFE (Immediately Invoked Function Expression) which allows us to eliminate the middle step of calling the outer function as we do it directly when assigning it.
const myModule = (function(){
const apiKey = "123456789"
return {
displayKey() {
console.log(apiKey)
}
}
})()
myModule.displayKey() // "123456789"
If we expose this module to another programmer, the API we prepared for him, does not allow him to change the key, he can only see it and there is no way he or she could change it other than rewriting it in the source code.
Basic caching and memoization
Imagine you would like to create a simple generator of IDs. To make sure you always return a number that is higher than the previous one, you can use closure. We will cache the value of the highest id in our current variable.
const newID = (function() {
let current = 0
return function() {
return ++current
}
})()
newID() // 1
newID() // 2
This idea of keeping track of certain data might be extremely useful when we are doing expensive computations, we can store parts of the results in a cache and when we do a computation with a higher number, we can use the data from our cache as a base. This process is called memoization. A prime example of that would be working with factorials or Fibonacci sequences.
const factorialMemo = (function() {
const cache = {}
return function factorial(n) {
if(n === 1 || n === 0) {
return 1
} else if (cache[n]) {
return cache[n]
} else {
cache[n] = n * factorial(n-1)
return cache[n]
}
}
})()
factorialMemo(5) //120
// cache object looks like {'2': 2, '3' : 6, '4' : 24, '5' : 120}
factorialMemo(6) // 6 * cached 120
Discussion (11)
There is no double function. So how is it works ?
double(); //10
double(); //20
The creator function is assigned to double here in this line
const double = creator(5)
double() //10
calling double is in essence calling the creator function.
Its calling what creator function returns not calling the creator ;)
There is, as the creator returns a function definition which you can call afterwards
Check twice bro ur gonna figure it out!
Nice post, but 0! = 1 or am I wrong?
What do you refer to?
EDIT: I can see it now, corrected, thanks
Eh, in Javascript you can never be too sure ;)
Forget it, if you need it, you learn it. It's not a big think, or hard to learn. But if youre goint to some interview, then learn it. Interviewers like this topic. :)
Everyone post tutorial in here! can I ask the question a post?
Yes you are welcome to ask questions related to this post. :)