Closure in JavaScript
A closure is a function that retains access to its outer function's variables, even after the outer function has finished executing. It "remembers" the environment in which it was created, allowing it to access variables outside its immediate scope.
Now let's understand this with the help of example
function outer() {
let outerVar = "I'm in the outer scope!";
function inner() {
console.log(outerVar);
}
return inner;
}
const closure = outer();
closure();
function outer() {
let outerVar = "I'm in the outer scope!";
function inner() {
console.log(outerVar);
}
return inner;
}
const closure = outer();
closure();
Output
I'm in the outer scope!
In this example
- The function inner() forms a closure by retaining access to outerVar, which is a variable in the scope of outer().
- Even though outer() has completed execution, inner() still has access to outerVar due to the closure.
Lexical Scoping
Closures are rely on lexical scoping, meaning that a function’s scope is determined by where the function is defined, not where it is executed. This allows inner functions to access variables from their outer function.
function outer() {
const outerVar = 'I am from outer';
function inner() {
console.log(outerVar);
}
return inner;
}
const newClosure = outer();
newClosure();
function outer() {
const outerVar = 'I am from outer';
function inner() {
console.log(outerVar);
}
return inner;
}
const newClosure = outer();
newClosure();
Output
I am from outer
In the example above, inner() has access to outerVar because inner was defined inside outer, giving it access to the outer function's scope.
Private Variables
Closures allow a function to keep variables hidden and only accessible within that function. This is often used when creating modules to protect certain data from being accessed or modified by other parts of the program.
function counter() {
// Private variable
let count = 0;
return function () {
// Access and modify the private variable
count++;
return count;
};
}
const increment = counter();
console.log(increment());
console.log(increment());
console.log(increment());
function counter() {
// Private variable
let count = 0;
return function () {
// Access and modify the private variable
count++;
return count;
};
}
const increment = counter();
console.log(increment());
console.log(increment());
console.log(increment());
Output
1 2 3
Closures and IIFE
IIFEs (Immediately Invoked Function Expressions) use closures to hide data inside the function. This helps keep certain information private and prevents it from being accessed outside the function, allowing you to create self-contained modules.
const counter = (function () {
let count = 0;
return {
increment: function () {
count++;
console.log(count);
},
reset: function () {
count = 0;
console.log("Counter reset");
},
};
})();
counter.increment();
counter.increment();
counter.reset();
const counter = (function () {
let count = 0;
return {
increment: function () {
count++;
console.log(count);
},
reset: function () {
count = 0;
console.log("Counter reset");
},
};
})();
counter.increment();
counter.increment();
counter.reset();
Output
1 2 Counter reset
Closure and setTimeout
Closures are helpful in asynchronous programming because they allow you to keep track of data even after a function has finished running. This is especially useful when you're working with things like timers or server requests, where the function might not run immediately.
function createTimers() {
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`Timer ${i}`);
}, i * 1000);
}
}
createTimers();
function createTimers() {
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`Timer ${i}`);
}, i * 1000);
}
}
createTimers();
Output
Timer 1
Timer 2
Timer 3
Closures with this keyword
Closures can be confusing when using the this keyword because this depends on how and where a function is called, not where it is defined. So, inside a closure, this might not refer to what you expect based on the function's location.
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name);
};
setTimeout(function () {
console.log(this.name);
// Undefined because 'this' refers to global object
}.bind(this), 1000);
// Fix with bind
}
const G = new Person("GFG");
G.sayName();
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name);
};
setTimeout(function () {
console.log(this.name);
// Undefined because 'this' refers to global object
}.bind(this), 1000);
// Fix with bind
}
const G = new Person("GFG");
G.sayName();
Function Currying in JavaScript (Closure Example)
Function currying is a technique to transform a function that takes multiple arguments into a series of functions that take one argument at a time. Currying relies on closures because each of the intermediate functions has access to the arguments passed previously.
In simple words, currying allows you to create specialized functions by partially applying arguments, which are remembered through closures.
// Normal Function
// function add(a, b) {
// return a + b;
// }
// console.log(add(2, 3));
// Function Currying
function add(a) {
return function(b) {
return a + b;
};
}
const addTwo = add(2); // First function call with 2
console.log(addTwo(3));
// Normal Function
// function add(a, b) {
// return a + b;
// }
// console.log(add(2, 3));
// Function Currying
function add(a) {
return function(b) {
return a + b;
};
}
const addTwo = add(2); // First function call with 2
console.log(addTwo(3));
Output
5
Common Pitfalls
- Memory Leaks: Excessive use of closures may retain unnecessary references to variables, causing memory issues.
- Performance Overhead: Overusing closures might lead to larger memory usage due to retained scopes.