Nested Functions and Closures
In JavaScript, a fascinating concept called "closure" comes into play when we nest a function inside another function. This nesting results in the inner function being considered private to the containing outer function. Consequently, the outer function can solely be invoked within its own context.
The true beauty of closure lies in its ability to grant the inner function access to the arguments and variables of its enclosing outer function. However, the reverse is not possible โ the outer function cannot reach into the variables of the inner function.
Due to its unique nature, closure is both an essential and challenging concept in JavaScript, often requiring a deeper understanding to fully grasp its potential and applications.
function sumSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
console.log(sumSquares(2, 3)); // 13
console.log(sumSquares(3, 4)); // 25
console.log(sumSquares(4, 5)); // 41
In this illustrative example, we have a function named sumSquares
, featuring an inner function called square
. The purpose of the "square" function is to multiply a given number by itself, effectively squaring it.
The sumSquares
outer function, designed to accept two parameters, cleverly employs the square
function twice, each time with a different parameter from the outer function. This way, it calculates and accumulates the sum of the squares of these two input parameters.
It is essential to grasp that this nesting concept can be applied recursively, allowing functions to be nested as many times as needed. In the following example, we have taken this concept further by demonstrating the nesting of three functions.
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1); // Logs 6 (which is 1 + 2 + 3)
Now let us see how the closure would work with an example below:
function multiply(x) {
function square(y) {
return x * y;
}
return square;
}
const fnInside = multiply(2); // gives a function that multiplies 2 to whatever you give it
console.log(fnInside(3)); // 13
console.log(multiply(2)(3)); // 13
In this illustration, the outer function takes a parameter and returns an inner function. This inner function, in turn, accepts another parameter and multiplies it by the parameter passed to the outer function. This mechanism relies on the closure property, allowing the inner function to access the parameters of the outer function.
To demonstrate the utilization of the closure property, let's examine the function invocations provided. The initial invocation calls the 'multiply' function with an argument of 2 and stores the returned square
function in the variable fnInside
. Subsequently, the inner function is invoked with an argument of 3, resulting in the multiplication of 3 by the value 2 previously passed to the outer function. The prior value of 2, which was provided to the multiply
function, is automatically retained in the context, thanks to the closure mechanism.
Let us now see a bit more complicated example of closures.
const createEmployee = function (name) {
let sex;
const person = {
setName(newName) {
name = newName;
},
getName() {
return name;
},
getSex() {
return sex;
},
setSex(newSex) {
if (
typeof newSex === "string" &&
(newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")
) {
sex = newSex;
}
},
};
return pet;
};
const e1 = createEmployee("Marie");
console.log(e1.getName()); // Marie
e1.setName("John");
e1.setSex("male");
console.log(e1.getSex()); // male
console.log(e1.getName()); // John
In the preceding example, the variables name
and sex
are created within the context and remain accessible to the object. Consequently, when we call the method getName()
, it returns the name Marie
, which was initially provided during the creation of the context.
However, it's worth noting that the variable sex
is currently undefined since it wasn't assigned any value or passed as an argument during the context creation. As a result, it holds no meaningful value at this point.
An intriguing aspect of this setup is the concept of closure. Whenever we invoke any method within the person
object, it retains access to the name
value from the outer context where it was defined. This behaviour showcases the powerful nature of closures, allowing the object to remember and utilize the data from its enclosing environment throughout its existence.