Javascript Closures
A closure is when an outer function returns an inner function, the inner function is then executed in a different scope, and the inner function continues to maintain access to the outer function's variables, even though the outer function no longer exists.
A closure allows the inner function that is returned access to all of the variables in the outer function which the inner function references, even though the outer function would have been destroyed by the time that the inner function is invoked.
Why use closures
- Encapsulation
- closures can hide variables of a function and selectively expose methods
- Init functions
- closures can be used to ensure that a function is only called once
- Memory optimization
- closures can be used to ensure that a large array or object is only initialized once
- Functional programming
- closures are a fundamental concept of functional programming. Without closures, higher-order functions and currying is not possible
Examples
Simple Closure
function greet() {
const name = 'John';
return function () {
console.log(`Hi ${name}`);
};
}
const greeting = greet();
greeting(); // Hi John
Count Function
function setCount() {
let number = 0;
return function () {
console.log(++number);
};
}
const counter = setCount();
counter(); // 1
counter(); // 2
counter(); // 3
For Loop Interview Question
function addNumbers() {
var numbers = [];
for (var i = 1; i <= 3; i++) {
numbers.push(function () {
return i;
});
}
return numbers;
}
const getNumbers = addNumbers(); // this is when the for loop runs to completion
console.log(getNumbers[0]()); // 4
console.log(getNumbers[1]()); // 4
console.log(getNumbers[2]()); // 4
Why?
- The var
i
in the for loop is hoisted- Creating a
var i;
declaration belowvar numbers = [];
- Creating a
- The value of
i
after the for loop ran to completion is 4i
iterates to 4 before the loop stops
- Invoking each anonymous function returns 4 because the anonymous function returns the value of
i
at the time of execution - Each anonymous function has access to the
i
variable because of closures
For Loop with Let
function addNumbers() {
var numbers = [];
for (let i = 1; i <= 3; i++) {
numbers.push(function () {
return i;
});
}
return numbers;
}
const getNumbers = addNumbers();
console.log(getNumbers[0]()); // 1
console.log(getNumbers[1]()); // 2
console.log(getNumbers[2]()); // 3
Using let
instead of var
in the for loop will have a unique binding for each iteration.
Think of this as there being a unique i
variable for each iteration.
For Loop with IIFE
const addNumbers = () => {
var numbers = [];
for (var i = 1; i <= 3; i++) {
((index) => {
numbers.push(() => {
return index;
});
})(i);
}
return numbers;
};
const getNumbers = addNumbers();
console.log(getNumbers[0]()); // 1
console.log(getNumbers[1]()); // 2
console.log(getNumbers[2]()); // 3
By wrapping each numbers.push
in an IIFE, we are ensuring that the index variable is privately scoped to this function, and as a result, the index variable will have a unique binding for each iteration.
Interview Question - setTimeout
const createCallbacks = () => {
for (var i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
};
createCallbacks();
// 4
// 4
// 4
- Invoking the
createCallbacks()
will run the for loop. - During each for loop iteration we create a
setTimeout
function.- Just creating the function (not invoking it)
- When we create a
setTimeout
function it is placed on the job queue- What javascript uses to keep track of when to run jobs
- The
createCallbacks
function will run to completion almost immediately - Javascript will keep running the event loop which keeps running continuously to check if any code needs to execute
- It will also check the job queue to see if there's any functions that needs to be executed at a specified time
- If they do, that function will be executed at that time.
The real question is how does each setTimeout
function have access to the i
variable?
The answer is, you guessed it, closures.
In this instance, setTimeout
is the inner function because it is accessing the i
variable which is hoisted outside of the for loop.
It's important to understand that closures are created when functions are created, not when they are invoked. And because a closure was created when this setTimeout
function was created, this enables the setTimeout
function to access the i
variable at whatever time the setTimeout
function will run.
Practical Examples
Encapsulation
const count = () => {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => console.log(count),
};
};
const counter = count();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
counter.getCount(); // 2
In the above example, we have successfully used a closure to implement encapsulation in javascript.
Using a Closure in an init function to ensure that the function is only executed once
const init = () => {
let initialized = false;
return () => {
if (initialized) {
return console.warn('⚠️ init function already called, not initializing');
}
initialized = true;
return console.info('initialized 🚀');
};
};
const initialize = init();
initialize();
initialize();
initialize();
The above example demonstrates how you can use a closure to ensure that code in an init function is only executed once, regardless of how many times the init function is invoked. This is a very useful pattern with various practical applications. For instance, you could use this to ensure that a database connection is only initialized once.
Inefficient Memory Usage
const findByIndex = (index) => {
console.time('array creation');
const numbers = Array.from(Array(1000000).keys());
console.timeEnd('array creation');
const result = numbers[index];
console.log(`item by index ${index}=${result}`);
return result;
};
findByIndex(110351);
findByIndex(911234);
findByIndex(520109);
findByIndex(398);
// output
// array creation: 61.145ms
// item by index 110351=110351
// array creation: 36.785ms
// item by index 911234=911234
// array creation: 41.395ms
// item by index 520109=520109
// array creation: 29.857ms
// item by index 398=398
The problem with the above function is that every time the findByIndex function is invoked, a new numbers array with a million items are created.
Efficient Memory Usage
const findByIndex = () => {
console.time('array creation');
const numbers = Array.from(Array(1000000).keys());
console.timeEnd('array creation');
return (index) => {
const result = numbers[index];
console.log(`item by index ${index}=${result}`);
return result;
};
};
const find = findByIndex();
find(110351);
find(911234);
find(520109);
find(398);
// output
// array creation: 52.937ms
// item by index 110351=110351
// item by index 911234=911234
// item by index 520109=520109
// item by index 398=398
In the above example, I changed the function so that it uses closures to ensure that the numbers array is only created once.
And if we look at the script that logs out the time, we can see that the numbers array is only created once, which improves performance.