Introduction
You may have heard about closures. You most certainly already use them even if you don't fully know what they are. Closures require you to know:
Three facts:
Fact One: In Javascript, you can refer to variables defined outside of the current function.
function makeCookies(){
const secretIngredient = "coconut oil"
function bake(chocolate){
return secretIngredient + " and " + chocolate
}
return bake("white chocolate")
}
makeCookies() // coconut oil and white chocolate
Here, we can see that the inner function bake can access the variable secretIngredient, even though it was defined in the outer makeCookies function.
Fact Two: Functions can refer to variables defined in outer functions even after those outer functions returned!
Because functions are first-class objects, you can store functions inside variables and call them later on. I've talked about higher-order functions in a https://dev.to/damcosset/higher-order-functions-in-javascript-4j8b
function cookiesMaker(){
const secretIngredient = "coconut oil"
function bake(chocolate){
return secretIngredient + " and " + chocolate + " chocolate."
}
return bake
}
const func = cookiesMaker() // Storing the function in a variable
Here, cookiesMaker is called and we store the result of that function inside a variable. If you print out the func variable right now, you would see the bake function.
The bake function uses a variable declared outside of the bake function (secretIngredient). The bake function can still remember that variable even if cookiesMaker has already returned.
func("black") // coconut oil and black chocolate.
func("white") // coconut oil and white chocolate.
How is this possible? Well, in Javascript, function values do not just store the code required to execute when they are called. They also store any references to variables they need to execute. Functions like the bake function, who refer to variables declared in their containing scopes are known as closures.
The bake function here keeps track of two variables declared in its containing scope: secretIngredient and chocolate.
When we call bake afterwards, it still remembers those two variables because there were stored in the closure.
A closure can refer to any variable or parameter in its scope. Check this out:
function cookiesBaker(cook){
return function addSecretIngredient(secretIngredient){
return function bakeCookie(chocolate){
return `${cook} cooked a ${secretIngredient} ${chocolate} chocolate cookie.`
}
}
}
In this example, the inner function bakeCookie refers to a parameter from the outer cookiesBaker function (cook), a parameter from the outer addSecretIngredient function (secretIngredient) and a parameter from its own scope (chocolate).
const cook = cookiesBaker("Damien")
const secret = cook("peanut butter")
const result = secret("white")
// Damien cooked a peanut butter white chocolate cookie.
Here, we are taking one more step.
We return the inner function addSecretIngredient and store that in a variable. Then, we call that stored function, the result ( bakeCookie ) is stored inside another variable. Finally, we call that function. The final results, as you can see, remembers all the variables stored inside the closure.
We can also use this to make more general-purpose functions.
Let's say we want to create a function for all cookies baked by Johnny:
const bakedByJohnny = cookiesBaker("Johnny")
bakedByJohnny("coconut oil")("black") // Johnny cooked a coconut oil black chocolate cookie.
bakedByJohnny("")("milk") // Johnny cooked a milk chocolate cookie.
Notice that instead of declaring a variable and storing the intermediate function inside it. I can call the inner function immediately because bakedByJohnny("coconut oil") is returning a function!
Ok, another little example. Let's create a function for all cookies baked by Sarah with peanut butter:
const bakedBySarahPeanutButter = cookiesBaker("Sarah")("peanut butter")
bakedBySarahPeanutButter("white")
//Sarah cooked a peanut butter white chocolate cookie.
bakedBySarahPeanutButter("black")
// Sarah cooked a peanut butter black chocolate cookie.
bakedBySarahPeanutButter("milk")
// Sarah cooked a peanut butter milk chocolate cookie.
Even though the two functions we created come from the same function definition, they are two distinct objects and both store different variables.
Note: The functions can be anonymous, like so:
let cookiesBaker = function(cook){
return function(secretIngredient){
return function(chocolate){
return `${cook} cooked a ${secretIngredient} ${chocolate} chocolate cookie.`
}
}
This code would give the exact same results than before!
Fact Three: Closures can not only remember the variable in their containing scope, they can also update it.
Consider the following example:
const secretIngredient = function(){
let ingredient = undefined
return {
changeIngredient: newIngredient => { ingredient = newIngredient },
showIngredient: () => ingredient,
type: () => typeof ingredient
}
}
This function returns 3 closures. Each method in the object returned refer to a variable defined in the containing scope.
Now, let's prove that closures can not only read outer variables, they can also update them:
let i = secretIngredient()
i.showIngredient() // undefined
i.type() // undefined
i.changeIngredient("coconut oil")
i.showIngredient() // coconut oil
i.type() // string
Tadaaaaa!
Conclusion
Closures are one of those things that you most likely use very often. You probably didn't even know about it! Check your code and try to identify closures, get comfortable with them, and use their full powers!