Closure is both a deceptively simple yet incredibly powerful part of JavaScript to grasp. The reason why callback functions are so powerful, the reason why asynchronous JS and everything it encompasses (Promises etc) are possible, is closure.
But what is Closure? Dan Abramov describes it best:
You have a closure when a function accesses variables defined outside of it.
For example, this code snippet contains a closure:
let users = ["Alice", "Dan", "Jessica"];
let query = "A";
let user = users.filter((user) => user.startsWith(query));
Notice how
user => user.startsWith(query)
is itself a function. It uses the query variable. But the query variable is defined outside of that function. That’s a closure.
How is this possible? It's because when you return a function from inside another function, you are not only return the function but also its "Variable Environment". This variable environment includes any variable or object that was declared in the outer function. The returned function retains a link to this outside variable environment. This link is more formally called Closed over ‘Variable Environment’ (C.O.V.E.)
or Persistent Lexical Scope Referenced Data (P.L.S.R.D.).
The concept can be a bit confusing, but we'll master it by going through these exercises together. As always, I'd recommend you solve the problems yourself first before looking at my solution, and then compare and contrast them.
Exercise 1
Create a function createFunction that creates and returns a function. When that created function is called, it should print "hello". When you think you completed createFunction, un-comment out those lines in the code and run it to see if it works.
function createFunction() {}
const function1 = createFunction();
function1();
// => should console.log('hello');
Solution 1
function createFunction() {
function printHello() {
console.log("hello");
}
return printHello;
}
A nice and easy start, yet this is the perfect demonstration of closure. We first call createFunction() and assign its value to function1
. function1
is now effectively the printHello() function as that is what was returned. We can now call function1() and it would execute the body of the printHello() function.
Exercise 2
Create a function createFunctionPrinter that accepts one input and returns a function. When that created function is called, it should print out the input that was used when the function was created.
function createFunctionPrinter() {}
const printSample = createFunctionPrinter("sample");
printSample();
// => should console.log('sample');
const printHello = createFunctionPrinter("hello");
printHello();
// => should console.log('hello');
Solution 2
function createFunctionPrinter(input) {
function printInput() {
console.log(input);
}
return printInput;
}
Very similar to the previous exercise, except here we are also demonstrating the concept of COVE or P.L.S.R.D. The inner function printInput()
gets access to the variables that were present in the outside function, in this instance the parameter input
.
Exercise 3
Examine the code for the outer function. Notice that we are returning a function and that function is using variables that are outside of its scope.
Try to deduce the output before executing.
function outer() {
let counter = 0;
// this variable is outside incrementCounter's scope
function incrementCounter() {
counter++;
console.log("counter", counter);
}
return incrementCounter;
}
const willCounter = outer();
const jasCounter = outer();
willCounter();
willCounter();
willCounter();
jasCounter();
willCounter();
Now create a function addByX that returns a function that will add an input by x.
function addByX() {}
const addByTwo = addByX(2);
console.log(addByTwo(1));
// => should return 3
console.log(addByTwo(2));
// => should return 4
console.log(addByTwo(3));
// => should return 5
const addByThree = addByX(3);
console.log(addByThree(1));
// => should return 4
console.log(addByThree(2));
// => should return 5
const addByFour = addByX(4);
console.log(addByFour(4));
// => should return 8
console.log(addByFour(5));
// => should return 9
Solution 3
function addByX(x) {
function addByNum(num) {
return num + x;
}
return addByNum;
}
We should be getting the hang of these type of functions. The first time addByX is called, it receives an argument and returns a function. This inner function will itself receive a parameter, but it will access to both, its own parameter and the addByX parameter, so it's able to whatever calculation is required on them both.
Exercise 4
Write a function once that accepts a callback as input and returns a function. When the returned function is called the first time, it should call the callback and return that output. If it is called any additional times, instead of calling the callback again it will simply return the output value from the first time it was called.
function once() {}
// /*** Uncomment these to check your work! ***/
const onceFunc = once(addByTwo);
console.log(onceFunc(4)); // => should log 6
console.log(onceFunc(10)); // => should log 6
console.log(onceFunc(9001)); // => should log 6
Solution 4
function once(func) {
let counter = 0;
let res = undefined;
function runOnce(num) {
if (counter === 0) {
res = func(num);
counter++;
}
return res;
}
return runOnce;
}
This is the first example where we can see how to use closure to give our functions a memory. By simply declaring a counter variable in the outside scope and then mutating it in the inner function, we can see how many times our function has been called and then have different behaviour based on the number of times the inner function has been called. This gives our functions a lot more flexibility and power, which we'll further explore in the following exercises.
Exercise 5
Write a function after that takes the number of times the callback needs to be called before being executed as the first parameter and the callback as the second parameter.
function after() {}
const called = function () {
console.log("hello");
};
const afterCalled = after(3, called);
afterCalled(); // => nothing is printed
afterCalled(); // => nothing is printed
afterCalled(); // => 'hello' is printed
Solution 5
function after(count, func) {
let counter = 0;
function runAfter() {
counter++;
if (count === counter) {
func();
}
}
return runAfter;
}
A similar example to the previous exercise, here we are just demonstrating a different behaviour. Again we can see that we can set a counter in the outside scope, using which we can determine how many times our function has been called. And based on that, we can implement different logic for our function.
Top comments (1)
Proposal solution for problem 4. You are asuming that return func will accept a number, when is not clear what function you are going to pass it in the first place. My proposal would be to have a cache and just call func is that cache is undefined. Otherwise we will return value save in cache: