In this article, we'll explore the concept of closures in JavaScript. We'll discuss what closures are, how they work, and how they can be used to create powerful and flexible code.
A function declared / defined inside another function has access to the outer function's scope, even after the outer function has returned. This is called a closure.
1function outerFunction() {
2 const outerVariable = 'I am from outer function';
3
4 return function () {
5 console.log(outerVariable);
6 };
7}
8
9const innerFunction = outerFunction();
10innerFunction(); // I am from outer function
innerFunction() still has access to the outerFunction() scope, even after outerFunction() has returned
A lexical environment is the environment in which a piece of code is executed. It consists of the variables that are in scope at that time, as well as a reference to the outer lexical environment. When a function is defined, it captures its lexical environment, creating a closure.
Diagram Loading...
This is what enables closures to work in JavaScript.
Closures enable use to create private variables in JavaScript. That cannot be directly accessed from outside the function.
1function createCounter() {
2 let count = 0;
3
4 return {
5 increment: () => {
6 count++;
7 return count;
8 },
9 decrement: () => {
10 count--;
11 return count;
12 },
13 };
14}
15
16const counter = createCounter();
17counter.increment(); // 1
18counter.increment(); // 2
19counter.count; // undefined
Closures help maintain the execution context of an asynchronous task.
The callback function passed to an asynchronous task has access to the variables in the outer scope, even after the outer function has returned.1function attachEventHandlers(buttonId) {
2 const button = document.getElementById(buttonId);
3
4 button.addEventListener('click', function () {
5 // buttonId is still accessible here
6 console.log(`Button ${buttonId} clicked`);
7 });
8}
9
10attachEventHandlers('btn-1');
11attachEventHandlers('btn-2');
Diagram Loading...
callBack() still has access to the buttonId variable, even after outerFunction() has returned
When using closures, be careful of shared scope issues. If multiple functions share the same outer scope, they will share the same variables.
1function createFunctions() {
2 const functions = [];
3
4 for (var idx = 0; idx < 3; idx++) {
5 functions.push(function () {
6 console.log(idx);
7 });
8 }
9
10 return functions;
11}
12
13const funcs = createFunctions();
14funcs[0](); // Output: 3 (not 0)
15funcs[1](); // Output: 3 (not 1)
16funcs[2](); // Output: 3 (not 2)
In this example, all the functions share the same idx value. Because var keyword is function-scoped instead of block-scoped.
In each iteration of the loop, the same idx variable is reassigned a new value.1for (let idx = 0; idx < 3; idx++) {
Switching from var to let fixes the issue.
You can learn more about the differences between var, let, and const in JavaScript in the following article:
When using closures, be mindful of memory leaks. If a closure holds a reference to a large object, it can prevent the object from being garbage collected.
1function createLargeClosure() {
2 let largeArray = new Array(1000000).fill(0);
3
4 return function () {
5 console.log("Closure created");
6 };
7}
8
9const largeClosure = createLargeClosure();
largeArray var is retained in memory, until the largeClosure func is no longer needed. (eg: by setting the value of largeClosure to null)
Closures are a powerful feature of JavaScript that enable us to create flexible and maintainable code.
By understanding how closures work and how to use them effectively, you can take your JavaScript skills to the next level. Just be mindful of the common pitfalls and best practices.