
Closures
Closures are one of the most powerful JavaScript features, but they can be a little daunting at first. Having a solid understanding of closures paves the way for understanding topics like higher-order functions and currying.
We’re going to address a few concepts that help illustrate the principles of closures, higher-order functions, and currying.
Functions in JavaScript are first-class citizens, which means that:
- Functions can be assigned to variables
- Functions can be passed as arguments to other functions
- Functions can return other functions
// functions can be assigned to variables
const morningGreetings = (name) => {
console.log(`Good morning ${name}`);
}
const eveningGreeting = function (name) {
console.log(`Good evening ${name}`);
}
// functions can be passed as arguments to other functions
const todaysGreeting = (morningGreetings, eveningGreeting) => {
morningGreetings('Barack')
console.log(`Thanks for all you have done during the day`);
eveningGreeting('Barack');
}
// functions can return other functions
function myCounter () {
let count = 0
return function () {
return ++count;
}
}
const noOfTimes = myCounter();
console.log(noOfTimes()); // 1
The feature we’ll look closely at allows functions to return functions. The feature’s closure depends upon the unique characteristics of JavaScript.
In JavaScript, functions have the ability to reference a variable that isn’t defined in the function but is available within an enclosing function or the global scope.
Consider the following example:
const iamglobal = 'available throughout the programme';
function funky() {
const iamlocal = 'local to the function scope funky';
}
console.log(iamglobal);// available throughout the programme
console.log(iamlocal); // iamlocal is not defined
As you can see, we’re unable to access the variable
iamlocal
outside of the scope of the function funky
. This is because the variable is only kept “alive” while the funky is active.
Once the function has been invoked, references to any variables declared within its scope are removed and the memory is handed back to the computer for use.
However, there’s a way we can have access to the variable declared within a function even after the function has been invoked.
This is where closures come in.
A closure is a reference to a variable declared in the scope of another function that is kept alive by returning a new function from the invocation of the existing function.
Let’s take a look at an example:
function outerScope() {
const outside = 'i am outside';
function innerScope() {
const inside = 'i am inside';
console.log('innerScope
', outside);
console.log('innerScope
',inside);
}
console.log('outerScope
', outside);
innerScope();
}
outerScope();
// outerScope
i am outside
// innerScope
i am outside
// innerScope
i am inside
It’s possible to access the value of the variable
outside
from function innerScope
. The concept of closures hinges on this capability.
From the example above, it’s possible for us to return the function
innerScope
rather than call it within the outerScope
, since this is close to a real world scenario.
Let’s modify the example above to reflect this change:
function outerScope() {
const outside = 'i am outside';
function innerScope() {
const inside = 'i am inside';
console.log('innerScope
', outside);
console.log('innerScope
',inside);
}
return innerScope
}
const inner = outerScope();
inner();
// outerScope
i am outside
// innerScope
i am outside
This resembles the example above, which illustrates how functions have the ability to return functions.
Let’s take this a step further and look at more of a real-world example:
function closure(a) {
return function trapB (b) {
return function trapC(c) {
return c * a + b;
}
}
}
const oneEight = closure(1.8);
const thirtyTwo = oneEight(32);
const degreeToFahrenheit = thirtyTwo(30);
console.log(degreeToFahrenheit); // 86
It can be useful to think of each function declaration as a circle in which each enclosing circle has access to the variables declared in the preceding circle:
In this case, trapC has access to variables
a, b and c
, while trapB has access to variable a and b
, and finally the closure has access only to a
.Higher-order functions
Higher-order functions are functions that accept another function as an argument, return another function as a result, or both.
So far, we’ve been using higher-order functions as seen in our
closure
, outerScope
,todaysGreeting
, and myCounter
examples.
Closures are integral to higher-order functions.
One of the core benefits of higher-order functions is that they allow us to customize the way we call our functions.
Consider the illustration below:
const multiply = (a , b) => {
return a * b;
}
console.log(multiply(2,3)) // 6
If we’re only interested in getting all the multiples of 2 throughout the entire program, you can repeat 2 as one of the arguments throughout our program:
multiply(2,1) // 2
multiply(2,2) // 4
multiply(2,3) // 6
While this works, it introduces a lot of repetition into our code and it violates the DRY (Don’t repeat yourself) principle.
You could also argue that we can hardcode the value of 2 into our function definition. Well, that’s true, but it’ll make our function less reusable.
Let’s redefine the function to use higher-order functions so we can see the benefits and flexibility it offers when calling the function:
const multiply = (a) => {
return (b) => {
return a * b;
}
}
Having defined the above function that way, we can create customize function calls as follows:
const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(3)) // 6
const multiplyByThree = multiply(3);
console.log(multiplyByThree(6)); // 18
We can create customize functions that have practical use and also save us the hassle of repeating ourselves.
Currying
Currying is a process that involves the partial application of functions.
A function is said to be curried when all the arguments needed for its invocation have not been supplied. In this case, it will return another function that retains the already-supplied arguments and expect the remaining omitted argument to be supplied before invoking the function.
The function is only invoked when all arguments have been supplied. Otherwise, a new function is returned that retains existing arguments and accepts new arguments as well.
When you curry a function, you call it as
f(a)(b)(c)(d)
rather than f(a, b, c , d)
. By extension, all curried functions are higher-order functions but not all higher-order functions are curried.
The bottom line here is that currying allows us to turn a single function into a series of functions.
Let’s consider the following example:
function sum (a, b) {
return a + b;
}
console.log(sum(4,5)) // 9
We can go ahead and curry this function so we have the flexibility to call it partially when all arguments aren’t supplied.
function curriedSum (x,y) {
if (y === undefined) {
return function(z) {
return x + z
}
} else {
return x + y;
}
}
console.log(curriedSum(4, 5)) // 9
console.log(curriedSum(4)(5)) // 9
We don’t have to write another curried implementation of our function every time we need it in order to call it partially. Instead, we can use a general curry function and pass our original function as an argument to it.
Here’s how:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
Let’s use an example to illustrate how this works.
function mean (a , b, c) {
return (a + b + c) / 3
}
const curriedMean = curry(mean);
console.log(curriedMean(1,2,3))
console.log(curriedMean(1,2)(3))
console.log(curriedMean(1)(2)(3))
Conclusion
As you can see, these concepts build upon one another since closures are used extensively in higher-order functions, and higher-order functions are similar to curried functions.
Comments
Post a Comment