Let's get started.
When you define a function, any variables within the function are only available from within the function. Attempting to access variables within a function from outside will result in a scope error; this is where closure comes in handy.
Let's look at scope with a code example showcasing variables declared in the global and local scopes.
In the above example, there are two scopes; the first is the global scope, where the variable "name" is declared. The second is the function's local scope, where the variable "message" is declared. In this code, the function can access the variable "name"; however, the function's local variable is only accessible to the function. If you try to access the function's local variable, the code will throw an error in response.
This behavior is important because it can help you understand how to avoid errors in your code, and it helps explain how lexical scoping works. It's interesting to note that Lexical scoping allows a nested scope can access a variable within its outer function. Let's see what that looks like in the code example below.
Any function declared at this level is technically a nested scope. Each function declared at the global level has its own scope and is not accessible from the global scope. Furthermore, the scope of a function is inaccessible from other functions. Let's look at that in the following code block.
Attempting to run the code above would result in an error message when the greetUser() function gets called.
Once the error occurs, the code will stop running to help keep debugging simple and make it easier to identify the cause of the error message.
In the following example, there are two instances of nested scopes; the first is the global scope, named so because it's the highest-level scope.
The global scope is where the message variable is declared. The greetUser() function exists within the buildGreeting() function; this means it's local to its parent function.
As a result of this local scoping, it also means that the greetUser() function is not available from the global scope. However, it's interesting to note that we can access the greetUser() function through the buildGreeting() function.
To make the greetUser() function accessible from the global scope, you return the greetUser() function from within the buildGreeting() function. Then you assign the buildGreeting() function to a variable and call that variable like a function.
In this example, you create a function called buildGreeting() and return a function that returns a string composed of two variables. The variables get passed in at two points in this code. The first is passed in when assigning the function to a variable, as seen below.
After the assignment, you call the variable a function passing in the following value as an argument for the inner function. In this case, the function call is performed in a console log, allowing you to see the string that the inner function creates.
In the above code, the loop runs three times, and the setTimeout function waits for a specified time to pass before running the code within it. You might expect the code to run three times, reflecting the id's index value at that loop's time.
However, in this case, the loops run, and the id variable updates accordingly. Since the code runs from the setTimeout function, the id has already been updated to its maximum value. Because all three iterations of the loop share the same scope, the setTimeout function creates a closure shared by each loop.
This fact means that the message printed is not what you expect. Instead, the console log reflects the final value of the id.
ES6 let Keyword
The code above results in the expected behavior instead of the setTimeout function running after the loop completes. The loop runs, and the setTimeout function gets assigned with the id to each iteration of the loop. You can see the resulting output below.
IIFE and Closures
Another way to avoid this issue with closures in a loop is to use the IIFE (Immediately Invoked Function Expression) syntax, which forces an immediate invocation of the setTimeout function as soon as the loop runs. So instead of essentially stacking the setTimeout function and waiting for the loop to finish, then execute the code, the setTimeout runs as soon as the loop starts, which is the expected behavior. Let's see what the syntax for IIFE looks like below.
In the above code, the loops run, and the function is invoked immediately on each iteration. The setTimeout function then starts to execute immediately, preserving the state of the id in each iteration. However, it is worth noting that the ES6 approach is a much cleaner solution to this issue; however, there may be times when IIFE works better.
When learning any programming concept, the best way to move forward is to practice what you've learned. Practice is even more critical with closures because the subject matter can be tricky. As a result of this fact, it is beneficial to explore and even create closures to learn what closures can look like in different situations. Each case is a little different, and you can use closures to perform many tasks that would otherwise be much more difficult. Identifying closures is the best way to solidify your understanding of closures and how to create them.