In some cases, closure can be tricky to predict. Gathered some of these concepts with code implementations to make the closure easy and interesting.
Definition
A function with a reference from the outer environment creates a closure.
- JS functions can access its lexical scope (outer scope)
- Functions can remember their lexical scope no matter where it is executed
Example and Tweaking
function outer () {
var a = 'hello';
function inner() {
console.log(a);
}
return inner;
}
outer()(); // prints "hello"
It is not necessary to define variables above the function definition.
function outer() {
function inner() {
console.log(a);
}
var a = 'hello'; // define variable after function definition
return inner;
}
outer()(); // prints 'hello'
Instead of using var
, we can use let
or const
instead.
function outer() {
function inner() {
console.log(a);
}
const a = 'hello';
return inner;
}
outer()(); // prints 'hello'
We can pass a parameter to the parent method and create a closure.
function outer(b) {
function inner() {
console.log(b);
}
return inner;
}
outer('I am param')(); // prints "I am param"
Closure can be formed with multiple hierarchies of methods.
function evenOuter() {
var val = 'From even outer';
function outer() {
function inner() {
console.log(val);
}
return inner;
}
return outer;
}
evenOuter()()();
Closure took the scoped variable and ignore the upper lexical scope,
var val = 'Global scope';
function outer() {
function inner() {
console.log(val);
}
var val = 'Outer scope';
return inner;
}
outer()(); // prints 'Outer scope'
Advantages of Closures
- Function currying
- Optimized higher-order function
- Memorization
- Data hiding and encapsulation
Function currying
By function currying, we can create new function for specialized argument and reuse them as needed.
function multiply(a) {
return function(b) {
return a * b;
}
}
// Currying the multiply function
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // Output: 10
console.log(triple(5)); // Output: 15
Higher Order Function
Higher order function takes another function as a parameter.
function repeat(fn, n) {
return function(x) {
for (let i = 0; i < n; i++) {
fn(x);
}
}
}
function logWithPrefix(prefix) {
return function(message) {
console.log(`${prefix}: ${message}`);
}
}
const repeatLog = repeat(logWithPrefix('INFO'), 3);
repeatLog('Hello'); // Output: INFO: Hello (three times)
Memorization
Similar to Function currying example.
Data Hiding and Encapsulation
Consider the following code,
let count = 0;
function increment() {
count += 1
console.log(count);
}
increment();
count = 2;
Here the major flow is, we can increment the count
directly without using the increment
method.
To ensure, always use the increment
to increment the count
, we can make use of closure.
function counter() {
let count = 0;
function increment() {
count += 1
console.log(count);
}
return increment;
}
const myIncrement = counter();
myIncrement(); // print 1
myIncrement(); // print 2
An interesting fact is, every time we call the counter
method, it always creates a new session.
function counter() {
let count = 0;
function increment() {
count += 1
console.log(count);
}
return increment;
}
const myIncrement1 = counter();
myIncrement1(); // prints 1
myIncrement1(); // prints 2
const myIncrement2 = counter();
myIncrement2(); // prints 1
myIncrement2(); // prints 2
If we need another function, decrement
then the code snippet is not scalable. In this case, we can make use of Function Constructor
.
function Counter() {
let count = 0;
this.increment = function() {
count += 1
console.log(count);
}
this.decrement = function () {
count -= 1;
console.log(count)
}
}
const myCounter = new Counter();
myCounter.increment(); // print 1
myCounter.increment(); // print 2
myCounter.decrement(); // print 1
Disadvantages
If not handled properly,
- Can cause overconsumption of memory
- Memory leaks