closures are a fundamental and powerful concept in JavaScript and many other programming languages. They allow functions to "remember" and access variables from their containing (enclosing) scope even after that scope has exited. To understand closures in detail, let's break down the key components and concepts associated with them.
Nested Functions: Closures arise from the use of nested functions, which means defining one function within another. The inner function is said to be a closure because it "closes over" the variables from the outer function's scope. [[Nested Functions]]
Lexical Scoping: JavaScript uses lexical Scoping, which means that the scope of a variable is determined by its location within the source code (the lexicon). In other words, it depends on where a variable is declared, not where it's called or used.
Accessing Variables: Closures can access variables from their containing (outer) function, even after the outer function has completed execution and its scope has technically exited.
Function as First-Class Citizens: In JavaScript, functions are first-class citizens, which means they can be assigned to variables, passed as arguments to other functions, and returned from functions. This flexibility is crucial for working with closures.
function outer() {
let outerVar = 10;
function inner() {
console.log(outerVar);
}
return inner;
}
const closureFunc = outer();
closureFunc(); // This still logs "10" even though outer() has completed.
In this example, inner
is a closure because it "closes over" the outerVar
variable from the outer
function. Even after outer
has completed execution, when we call closureFunc()
, it can still access and log the value of outerVar
.
Use Cases for Closures: Closures have several practical applications, such as:
- Data encapsulation and information hiding: You can use closures to create private variables and methods.
- Function factories: You can create functions that generate other functions with specific behavior.
- Callbacks and event handling: Closures are commonly used to manage callbacks and event handlers.
- Memoization: Closures can be used to cache results of expensive function calls.
- Module patterns: Closures enable the creation of modules with private and public parts.
Closure Related Issues and Solution through IIFE
consider this code :
for (var i = 1; i <= 4; i++) {
setTimeout(function () {
console.log(i);
}, i * 10000);
}
//console output
//5(4 times)
In this code, we have a loop that runs from i
values 0 to 4. Inside the loop, there's a setTimeout
function that logs the value of i
after a 1-second delay. You might expect this code to log 0, 1, 2, 3, and 4, each after a 1-second delay. However, it doesn't work as expected. It logs the value 5 five times after 1 second because the loop variable i
is shared among all the timeouts.
Here's why this happens:
- The loop runs and increments
i
from 0 to 4. - The
setTimeout
callbacks are scheduled to run after a 1-second delay, but they don't execute immediately. - By the time the callbacks run, the loop has already finished, and
i
has a final value of 5.
So, all the setTimeout
callbacks capture the same i
, which is 5 when they execute. This behavior is because closures in JavaScript capture variables by reference, not by value. Therefore, all the callbacks see the same i
, which is 5.
To solve this issue, developers often use the approach shown in your initial code snippet:
for (var i = 1; i <= 4; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, j * 1000);
})(i);
}
In this version, an [[immediately invoked function expression (IIFE)]] is used to create a new scope for each iteration of the loop. The j
variable inside the IIFE captures the current value of i
for that specific iteration, preventing the closure over the loop variable problem. As a result, each setTimeout
callback logs the correct value of j
.
The closure related issues due to difference between using let and var
var
: Variables declared with var
in a loop often lead to unintended closure-related issues, as we saw in your initial code snippet. This is because they capture the variable by reference and not by value, causing all closures to share the same reference.
let
: Variables declared with let
in a loop create a new variable with each iteration, effectively capturing the value of the variable at that iteration. This behavior is why let
is often preferred in situations where you want to avoid closure-related problems.
// Using var
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
With var
, there's a single i
variable with function scope. All five setTimeout
callbacks capture the same i
by reference, so when they execute, they all see the final value of i
, which is 5.
// Using let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
With let
, there are five separate i
variables with block scope, one for each iteration. Each setTimeout
callback captures its own i
, and when they execute, they log the value of i
from their respective block scopes, resulting in the expected output of 0, 1, 2, 3, and 4
let
provides block-scoping, which is well-suited for situations like loop counters, where you want to avoid closure-related issues and ensure that each iteration has its own isolated variable. var
, on the other hand, is function-scoped and can lead to unintended behavior in certain situations.
in order to better understand this read [[Difference between let and var]]
Emulating private methods with closures
JavaScript, prior to classes, didn't have a native way of declaring private methods, but it was possible to emulate private methods using closures. Private methods aren't just useful for restricting access to code. They also provide a powerful way of managing your global namespace.
const counter = (function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
})();
console.log(counter.value()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.
each closure had its own lexical environment. Here though, there is a single lexical environment that is shared by the three functions: counter.increment
, counter.decrement
, and counter.value
.
The shared lexical environment is created in the body of an anonymous function, which is executed as soon as it has been defined(also known as [[immediately invoked function expression (IIFE)]])
The lexical environment contains two private items: a variable called privateCounter
, and a function called changeBy
. You can't access either of these private members from outside the anonymous function. Instead, you can access them using the three public functions that are returned from the anonymous wrapper.
Those three public functions form closures that share the same lexical environment. Thanks to JavaScript's lexical scoping, they each have access to the privateCounter
variable and the changeBy
function.
const makeCounter = function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value()); // 0.
counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.
counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
Notice how the two counters maintain their independence from one another. Each closure references a different version of the privateCounter
variable through its own closure. Each time one of the counters is called, its lexical environment changes by changing the value of this variable. Changes to the variable value in one closure don't affect the value in the other closure
Top comments (4)
Thanks, really great examples!
I needed a lot of time to understand lexical scoping, and still don't quite get how exactly it works under the hood
It happens with my too, You can understand lexical scoping in several analogies, Maybe I will make one post to simplify the lexical scoping too
Cool
Unfortunately, some of this post is incorrect. There are a number of widespread misconceptions about closures:
Misconceptions About Closures
Jon Randy 🎖️ ・ Sep 27