JavaScript closures are constructs that allow nested functions to be written, allowing the inner function to access the outer function’s variables. In this way, data sharing between different functions is facilitated and changes in one function are reflected in other functions. Also, closures help make code more modular and scalable. In this article, we’ll take an in-depth look at JavaScript closures and learn about their use, advantages, and examples.
Before we deal with the subject in detail, we need to touch on Lexical scoping. In this way, it will be much easier to understand the subject.
Lexical Scoping
In JavaScript, Lexical scoping is used when determining the scope of a function. This means that a variable is scoped based on where it is defined. That is, a variable defined inside a function is valid inside that function and within the scope of functions defined within it. However, it cannot access the scope of outside functions. For example, let’s examine the following code:
function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
let innerVariable = 'I am inside!';
console.log(innerVariable); // Output: "I am inside!"
console.log(outerVariable); // Output: "I am outside!"
}
innerFunction();
}
outerFunction();
In this code, innerFunction function is defined inside outerFunction function. So the scope of innerFunction also includes the scope of outerFunction. Therefore, the variables innerFunction, innerVariable, and outerVariable are accessible. However, it is not possible to access these variables from outside the outerFunction.
Scoping in JavaScript: Let & Const
In JavaScript, var, let, and const keywords are used to declare variables.
Variables defined with var can be defined in function scope or global scope. However, they do not provide block coverage. That is, a var variable can be accessed outside the block in which it is defined. This can make the code less reliable and cause errors.
let and const are new keywords that come with ES6. Variables defined with let and const are defined at block scope. That is, a let or const variable cannot be accessed outside of the block in which it is defined. This makes the code more reliable.
The difference between let and const is whether the variable’s value is mutable or not. The values of variables defined with let can be changed, while the values of variables defined with const are fixed and cannot be changed later.
function sayHello() {
var name = "John";
if (true) {
var name = "Jane";
console.log(name); // "Jane"
}
console.log(name); // "Jane"
}
sayHello();
In the example above, a var variable named name is defined inside the sayHello function with an initial value of “John”. Later, another var variable named name is defined inside the if block with an initial value of “Jane”. However, the important point to note is that the name variable inside the if block changes the value of the name variable outside the function.
Let’s now examine the same example for let.
function sayHello() {
let name = "John";
if (true) {
let name = "Jane";
console.log(name); // "Jane"
}
console.log(name); // "John"
}
sayHello();
In the above example, the inner let variable name provides block scoping and is only valid within that block. The outer let name variable is valid within the function’s main scope. Thus, the first console.log statement returns the result “Jane”, while the second console.log statement returns the result “John”.
The topic of closures is closely related to lexical scoping. When closures are used, variables and functions defined within a function remain within the scope of that function and cannot be used elsewhere. This is one of the fundamental principles of lexical scoping. Now that we have briefly touched on the subject of lexical scoping, we can return to our main topic.
Closure
In JavaScript, functions can be assigned to variables like other data types, can be returned as the result of a function, and can be used inside other functions.
A variable defined inside a function is valid only within its scope. That is, code outside the function cannot access this variable.
Closure refers to the situation where the inner function can access the scope of the outer function and the variables defined in it can be used in this scope.
Also, after a closure is created, the values of these variables are retained until the end of the runtime of the functions called from outside to access the variables inside. In this way, variables defined within a function remain accessible even after the function has finished running.
function counter() {
let count = 0;
function incrementCount() {
count++;
console.log(count);
}
return incrementCount;
}
const myFunc = counter(); // Closure is created
myFunc(); // output: 1
myFunc(); // output: 2
myFunc(); // output: 3
In the example above, the counter() function returns a closure. This closure is the incrementCount() function that accesses the count variable defined in it. The counter() function we assign to the myFunc variable returns the incrementCount() function, creating a closure.
When myFunc()is called afterwards, the value of the count variable inside the closure is incremented and printed to the console. Since this function works in conjunction with the closure returned by the counter()function, it retains the final value of the count variable. This means that as long as myFunc()is called, the value of the count variable will be maintained and incremented.
Let’s continue with another example now.
function myFunc(x) {
return function(y) {
return x + y;
}
}
const sum = myFunc(4)(5);
console.log(sum); // output: 9
In this code, a function named myFunc is defined that takes in one argument x. This function returns another function that takes in an argument y and returns the sum of x and y.
In the next line, the myFunc function is called with the argument 4, which returns a new function that adds 4 to any value passed in as y. Then, this newly returned function is immediately called with the argument 5, resulting in the sum of 4 and 5, which is 9.
Finally, the value of sum (which is 9) is logged to the console using console.log().
This way, myFunc function creates a closure by returning an inner function that can access the outer function's variables. Thus, the returned function can be called with different parameters and each function call will create its own closure with its own set of parameters.
The following example is similar to the closure example in the previous one. When the outerFunc function is called, the value of xis stored within the function’s scope, and innerFunc1 function is returned. Then, when the innerFunc1 function is called with the y value, the value of y is also stored in the closure, and innerFunc2 function is returned. Finally, when the innerFunc2 function is called with the z value, x, y and z values are added together and the result is returned.
function outerFunc(x) {
function innerFunc1(y) {
function innerFunc2(z) {
return x + y + z;
}
return innerFunc2;
}
return innerFunc1;
}
const sum = outerFunc(4)(5)(2);
console.log(sum); // output: 11
Lifetime and Memory Usage of Functions
When a closure is created, the variables and functions inside the closure occupy a memory space just like other objects in memory.
For example, in the following code block, an innerFunction is defined inside the outerFunction, and this inner function uses a variable called innerVariable.
function outerFunction() {
var innerVariable = 10;
function innerFunction() {
console.log(innerVariable);
}
return innerFunction;
}
var myClosure = outerFunction();
A closure is created by assigning the outerFunction function to the variable named myClosure. In this case, innerVariable and innerFunction are retained in memory and are referenced by the myClosure variable.
However, the innerVariable variable and the innerFunction function use memory unnecessarily because they are referenced by the myClosure variable. This can result in large memory usage when many shutdowns are used in the program.
For instance, in the code block below, the outerFunction is called 1000 times, and each call creates a closure. Each closure occupies a memory space, and this increases the memory usage.
function outerFunction() {
var innerVariable = 10;
function innerFunction() {
console.log(innerVariable);
}
return innerFunction;
}
for (var i = 0; i < 1000; i++) {
var myClosure = outerFunction();
}
Therefore, caution should be exercised when using closures and not creating too many closures unnecessarily.
References and Garbage Collection
Closures held in memory are deleted when they are out of reference or cleared by the garbage collector. If a closure references a local variable inside a function and that reference still exists after the function has finished running, the closure remains in memory and remains accessible. However, the closure is cleared from memory when there is no reference left or it is cleared by the garbage collector.
For example, if you assign a closure to a variable and then assign that variable to null or some other value, the reference to the closure is lost and the closure is cleared from memory.
The following example shows how the reference of a closure is destroyed:
function outerFunction() {
var outerVariable = 'Hello';
return function() {
console.log(outerVariable);
}
}
var myClosure = outerFunction();
myClosure(); // "Hello"
myClosure = null; // Closure is destroyed
In this example, myClosure represents a closure and is a function returned from outerFunction. The myClosure function references a local variable named outerVariable. However, when myClosure’s reference is set to null, the closure is no longer referenced and is cleared from memory by the garbage collector.
Closure Performance: How Can We Get Better Performance?
Closures are frequently used in many different use cases in JavaScript and are an essential tool for code readability and flexibility. However, they can cause performance issues if not used properly.
Closures can run slower than other functions because they create a new scope with each call. Also, having a closure in memory can cause unnecessary memory usage and slowdown.
However, there are some techniques to avoid performance issues with closures. The first is to use closures as little as possible. Especially when you are in a loop that requires frequent access to variables inside a closure, it is better to use local variables that can be used as loop variables rather than creating a new closure each time.
It is also important to keep closures as small as possible so that they do not cause unnecessary memory usage. That is, it should not unnecessarily contain too many variables or have unnecessary features.
Finally, to avoid unnecessary memory usage of a closure, it is recommended to delete its reference or set it to null when the closure is finished using it. This helps the JavaScript engine avoid unnecessary memory usage.
In summary, when used correctly, closures can be a very useful feature in JavaScript. However, they can cause performance issues if misused. Keeping Closures as small and optimized as possible and their references managed properly can help avoid performance issues.
In this article, we explained what closures are, how they work, how they affect memory usage, and when they are cleared from memory.
I created the code samples and explanations in this article using chatgpt. I wrote the article by just proceeding with chatgpt without any preliminary research on the subject from any source (books, websites, etc.). Chatgpt can give wrong information in some cases, but in these cases, I was able to proceed easily by guiding chatgpt correctly. I hope we were able to convey the subject in a nice and detailed way with chatgpt :)
Top comments (0)