JS Closures

Understanding the JavaScript Closures

In the JavaScript functions chapter, you discovered that in JavaScript, a variable's scope can be either global or local. Since ES6, you can also define block-scoped variables using the let keyword.

A global variable can be accessed and changed anywhere in the program, whereas a local variable can only be accessed and modified within the function where it is declared.

However, there are situations where you need a variable to be accessible throughout the script, but you don't want just any part of your code to accidentally change its value.

Let's see what happens if you try to accomplish this with a global variable:

// Global variable
var counter  = 0;

// A function dedicated to manipulate the 'counter' variable
function makeCounter() {
return counter += 1;
}

// Calling the function
makeCounter();
console.log(counter); // Prints: 1

makeCounter();
console.log(counter); // Prints: 2

// Trying to manipulate the 'counter' variable from outside
counter = 10;
console.log(counter); // Prints: 10

As shown in the example above, the value of the counter variable can be altered from any part of the program, without needing to call the makeCounter() function (line no-17).

Now, let's attempt to do the same with a local variable and observe the results:

function makeCounter() {
// Local variable
var counter  = 0;

// Manipulating the 'counter' variable
return counter += 1;
}

// Calling the function
console.log(makeCounter()); // Prints: 1
console.log(makeCounter()); // Prints: 1

In this scenario, the counter variable cannot be changed from outside, as it is local to the makeCounter() function. However, its value won't increase with subsequent function calls because each time the function is called, it resets the counter variable, as seen in the example above (line no-11). JavaScript closures can solve this issue.

A closure is essentially an inner function that has access to the outer function's scope, even after the outer function has finished executing. This is achieved by creating a function inside another function. Let's look at the following example to understand how it works:

function makeCounter() {
var counter = 0;

// Nested function
function make() {
counter += 1;
return counter;
}
return make;
}

/* Execute the makeCounter() function and store the
returned value in the myCounter variable */
var myCounter = makeCounter();

console.log(myCounter()); // Prints: 1
console.log(myCounter()); // Prints: 2

As observed in the example above, the inner function make() is returned by the outer function makeCounter(). Therefore, myCounter holds a reference to the inner make() function (line no-14), and invoking myCounter effectively calls make(). In JavaScript, functions can be assigned to variables, passed as arguments to other functions, nested inside other functions, and more.

It's also noticeable that the inner function make() retains access to the counter variable defined in its outer function, even after the execution of makeCounter() has completed (line no-14). This behavior is due to closures in JavaScript, where functions internally hold references to their outer variables and can both access and update their values.

In the example, the make() function acts as a closure, referencing the outer variable counter. This means whenever make() is called, it can access and modify the counter variable because it's encapsulated within the closure.

Once the outer function finishes execution, the counter variable becomes inaccessible to other parts of the code. Only the inner function, enclosed within the closure, retains exclusive access to it.

The previous example can also be rewritten using an anonymous function expression, shown below:

// Anonymous function expression
var myCounter = (function() {
var counter = 0;

// Nested anonymous function
return function() {
counter += 1;
return counter;
}
})();

console.log(myCounter()); // Prints: 1
console.log(myCounter()); // Prints: 2

Tip: In JavaScript, every function has access to the global scope and any scopes above it. Since JavaScript supports nested functions, this means nested functions can access values declared in higher scopes, including their parent functions.

Note: Global variables persist as long as your application (i.e., your web page) is running. In contrast, local variables have a shorter lifespan: they are created when a function is called and are destroyed as soon as the function finishes executing.


Creating Getter and Setter Functions

Here we will create a variable secret and protect it from direct manipulation by external code using closures. We will also define getter and setter functions to access and modify its value.

Furthermore, the setter function will include a quick validation to ensure that the provided value is a number. If it is not a number, the variable value will not be updated.

var getValue, setValue;

// Self-executing function
(function() {
var secret = 0;

// Getter function
getValue = function() {
return secret;
};

// Setter function
setValue = function(x) {
if(typeof x === "number") {
secret = x;
}
};
}());

// Calling the functions
getValue(); // Returns: 0
setValue(10);
getValue(); // Returns: 10
setValue(null);
getValue(); // Returns: 10

Tip: Self-executing functions are also known as immediately invoked function expressions (IIFE), immediately executed functions, or self-executing anonymous functions.