Blog> Categories: Functional-Programming, JavaScript
Table of Contents
What is a Closure? #
A closure in JavaScript is a function that remembers and accesses variables from its outer scope even after the outer function has finished executing. This means that a function defined inside another function has access to the outer function’s variables, and this access is preserved even after the outer function has returned. Closure is a core concept of functional programming.
How Closures Work in JavaScript #
Lexical Scoping #
To understand closures, it’s essential first to understand the concept of lexical scoping. In JavaScript, the scope of a variable is defined by its location within the source code (lexical scope). A function’s scope includes the function’s own local variables, parameters, and variables from the enclosing scope(s).
Creation of a Closure #
When a function is defined, it retains a reference to its lexical environment, which includes variables in the scope where the function was created. This reference forms the closure.
Example #
Consider the following code:
function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable); // Accessing a variable from the outer scope
}
return innerFunction;
}
const myFunction = outerFunction();
myFunction(); // Logs: "I am outside!"
In this example:
outerFunction
defines a variableouterVariable
and an inner functioninnerFunction
.innerFunction
accessesouterVariable
, creating a closure.outerFunction
returnsinnerFunction
, and the returned function is assigned tomyFunction
.- When
myFunction
is called, it still has access toouterVariable
from the scope ofouterFunction
, even thoughouterFunction
has already finished executing.
Role of Closure #
Closures play a significant role in functional programming. Functional programming is a programming paradigm that emphasizes the use of pure functions, immutability, and higher-order functions. Higher-order functions are functions that can take other functions as arguments or return functions as their results.
Closures are used to create higher-order functions that can generate new functions based on their inputs. In functional programming, these higher-order functions are often used to transform data or to create new functions that have specific behaviors based on the input data.
Closures are also used to create private variables and functions. By encapsulating variables and functions within a closure, you can limit their scope and prevent them from being accessed from outside the closure. This is particularly useful in situations where you want to avoid naming conflicts or prevent external code from modifying your variables or functions.
Closures can also be used to implement currying, which is the process of transforming a function that takes multiple arguments into a series of functions that each take a single argument. This technique can make functions more modular and easier to reuse.
Importance of Closure #
Closures are important in JavaScript because they provide a way to create private variables and functions in a function.
By encapsulating variables and functions within a closure, you can limit their scope and prevent them from being accessed from outside the closure. This is particularly useful in situations where you want to avoid naming conflicts or prevent external code from modifying your variables or functions.
Closures are also used extensively in functional programming to create higher-order functions that can generate new functions based on their inputs. This is because closures can capture the state of a function at a particular point in time, allowing for more flexible and powerful functions.
Additionally, closures can help to reduce memory usage by allowing variables and functions to be garbage collected once they are no longer in use.
Examples of Closure #
Example 1: Basic Example #
function outerFunction(name) {
const outerVariable = "Hello "+name+"! I am in the outer function's scope.";
function innerFunction() {
return(outerVariable);
}
return innerFunction;
}
const inner = outerFunction("User");
inner(); // Output: "Hello User! I am in the outer function's scope."
This code example demonstrates how closures in JavaScript allow an inner function to retain access to variables from its outer function’s scope even after the outer function has completed execution. This is useful for creating functions that can maintain state or access variables in a controlled manner.
Closure Explanation #
- When
outerFunction
is called with"User"
, it creates an execution context withname
set to"User"
andouterVariable
set to"Hello User! I am in the outer function's scope."
. innerFunction
is defined within this context and captures the variables within its lexical scope, which includesouterVariable
.- Even after
outerFunction
finishes executing,innerFunction
retains access toouterVariable
because of the closure. This allowsinnerFunction
to returnouterVariable
when it is later called asinner
.
You can play around with this example and other in this notebook: JavaScript Notebook for Closures
Example 2: Creating Private Variables #
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
Code Explanation #
This code demonstrates the use of closures in JavaScript to create a simple counter. The createCounter
function initializes a count
variable and returns an inner function that increments and returns this count
variable. Each time the returned function (counter
) is called, it updates and returns the count
. The inner function retains access to the count
variable even after createCounter
has finished executing, thanks to the closure created by the inner function. This allows the counter to maintain its own independent state across multiple calls.
- Define the
createCounter
Function:function createCounter() { let count = 0; return function() { return ++count; }; }
createCounter
is a function that, when called, initializes a local variablecount
to0
.- Inside
createCounter
, an anonymous inner function is defined and returned. This inner function has access to thecount
variable due to JavaScript’s closure mechanism.
- Return the Counter Function:
return function() { return ++count; };
- The inner function uses the increment operator (
++
) oncount
before returning it. Each call to this function will increment thecount
variable by 1 and then return the new value ofcount
.
- The inner function uses the increment operator (
- Create a Counter Instance:
const counter = createCounter();
createCounter
is called, and the returned inner function (the counter function) is assigned to the variablecounter
.- At this point,
counter
is a function that has access to its owncount
variable, which is initialized to0
withincreateCounter
.
- Call the Counter Function:
console.log(counter()); // Output: 1 console.log(counter()); // Output: 2
- The first call to
counter()
increments thecount
from0
to1
and returns1
, which is logged to the console. - The second call to
counter()
increments thecount
from1
to2
and returns2
, which is logged to the console.
- The first call to
Example 3: Implementing Memoization #
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (!cache[key]) {
cache[key] = func(...args);
}
return cache[key];
};
}
const expensiveCalculation = memoize(function(n) {
console.log('Performing expensive calculation...');
return n * n;
});
console.log(expensiveCalculation(5)); // Output: Performing expensive calculation... 25
console.log(expensiveCalculation(5)); // Output: 25 (result retrieved from cache)
Code Explanation #
This code demonstrates how to create a memoization function in JavaScript to optimize performance by caching results of expensive function calls. The memoize
function wraps an existing function and adds a caching mechanism using an object (cache
). When the memoized function is called, it checks if the result for the given arguments is already in the cache. If so, it returns the cached result; otherwise, it computes the result, caches it, and then returns it. This technique can significantly improve performance for functions with repetitive and costly computations.
- Define the
memoize
Function:function memoize(func) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (!cache[key]) { cache[key] = func(...args); } return cache[key]; }; }
memoize
is a higher-order function that takes a functionfunc
as an argument and returns a new function.- Inside
memoize
, an empty objectcache
is created to store the results of function calls.
- Return the Memoized Function:
return function(...args) { const key = JSON.stringify(args); if (!cache[key]) { cache[key] = func(...args); } return cache[key]; };
- The returned function uses the rest parameter syntax (
...args
) to collect all its arguments into an array. - It creates a
key
by converting theargs
array into a JSON string. Thiskey
uniquely identifies the combination of arguments. - It checks if the
cache
object already has a value for thiskey
. If not, it calls the originalfunc
with theargs
and stores the result incache[key]
. - Finally, it returns the cached result.
- The returned function uses the rest parameter syntax (
- Memoize an Expensive Function:
const expensiveCalculation = memoize(function(n) { console.log('Performing expensive calculation...'); return n * n; });
expensiveCalculation
is a memoized version of the function that calculates the square of a numbern
. The memoized function will cache the results to avoid redundant calculations.
- Call the Memoized Function:
console.log(expensiveCalculation(5)); // Output: Performing expensive calculation... 25 console.log(expensiveCalculation(5)); // Output: 25 (result retrieved from cache)
- The first call to
expensiveCalculation(5)
logs “Performing expensive calculation…” and returns25
because it calculates5 * 5
. - The second call to
expensiveCalculation(5)
does not log “Performing expensive calculation…” because the result25
is retrieved from the cache.
- The first call to
Practical Use: Moments (statistics) #
In this section we will build code for calculate moments from a set of numbers. k-th raw moment is defined in statistics as mean(x^k), where x is a number from a population of random numbers. Mean can be calculated as sum(x^k)/n, where n is size of the population. First we will define the population
const population = [1,2,3,5,8,2,6,3,5,10,7,4] //Can be any sequence
We, will create an encapsulated power function:
power_k=function(k){
return x=>x^k;
} //The power_k returns a function that encapsulates k using closure.
We, will calculate the sequence of powers using the Array function map: const k=3; const powers= population.map(power_k(3))
We, can define mean using Array function reduce:
mean = function(arr){
const sum = arr.reduce((accumulator, num) => {
return accumulator + num;
}, 0);
return sum/arr.length
} Finally, the k'th moment is:
k_th_moment = mean(powers) //Output 4.333333333333333
Now that we have the power function and mean function defined, we can easily calculate any moment in a single line. For example, if you want 5th moment, you can do so in a single line:
console.log(mean(population.map(power_k(5)))) //Output 5.333333333333333
There are several interesting uses of such closure. A more sophisticated example could be where one wants to simulate the behavior of gas with fixed volume and mass but varying temperature. Then the behavior can be enclosed as a function of temperature with the volume and mass encapsulated as data.
A detailed discussion higher order function with an example involving closure is in this article: Higher Order Functions in Functional Programming using JavaScript.
Applications of Closure in JavaScript #
Closures are a fundamental and powerful concept in JavaScript, enabling functions to remember and access variables from an outer function’s scope even after the outer function has finished executing. This unique feature opens the door to a variety of practical and advanced programming techniques. Here are some key applications of closures in JavaScript:
1. Data Privacy #
Closures are often used to create private variables and methods. This is a form of data encapsulation, where internal details of an object or function are hidden from the outside world.
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.decrement()); // 0
2. Function Factories #
Closures enable the creation of function factories, where a function returns other functions with preset configurations. This technique is useful for generating multiple similar functions without redundant code.
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. Event Handlers #
Closures are essential in event handling, especially when dealing with asynchronous code or callbacks. They allow you to retain access to variables that were in scope when the event handler was defined.
function setup() {
let name = 'Closure Example';
document.getElementById('btn').addEventListener('click', function() {
alert('Button clicked in ' + name);
});
}
setup();
4. Memoization #
Memoization is an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again. Closures provide a convenient way to store these results.
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const fibonacci = memoize(function(n) {
if (n < 2) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // 55
5. Iterators and Generators #
Closures can be used to create iterators and generators, allowing for customized iteration behavior.
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next: function() {
if (nextIndex < end) {
let result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
}
};
return rangeIterator;
}
const it = makeRangeIterator(1, 10, 2);
let result = it.next();
while (!result.done) {
console.log(result.value); // 1 3 5 7 9
result = it.next();
}
6. Module Pattern #
Closures are often used to create modules, encapsulating functionality and exposing only the parts that need to be public.
const calculatorModule = (function() {
let total = 0;
return {
add: function(x) {
total += x;
return total;
},
subtract: function(x) {
total -= x;
return total;
},
getTotal: function() {
return total;
}
};
})();
console.log(calculatorModule.add(10)); // 10
console.log(calculatorModule.subtract(5)); // 5
console.log(calculatorModule.getTotal()); // 5
7. Currying #
Currying is a technique where a function is transformed into a sequence of functions, each with a single argument. Closures are instrumental in implementing currying.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
Closures in JavaScript are a versatile and powerful feature, enabling various advanced programming techniques and design patterns. From data privacy and function factories to memoization and currying, closures provide a robust toolset for developers to write clean, efficient, and maintainable code. Understanding and mastering closures unlocks the full potential of JavaScript as a functional programming language.