Understanding JavaScript Closures

Understanding JavaScript Closures
Computer Science
Reading Time: 4 minutes

Patrick Carey is a Cengage author and presenter and has authored or co-authored over 20 academic and trade texts for the software industry.

 

Closures are one of the most important and most misunderstood concepts by students in JavaScript programming. If your students are in a job interview, there is a good chance they will have to explain closures. They may also have to give some examples of code which involve closures. A formal definition of closure, care of Mozilla.org, is:

closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)

 

To help your students fully understand what closures mean and how to apply them to programming projects, I find it best to start by explaining the concept of scope.

 

Variables and Scope

When a variable is defined, its existence is restricted by its scope. Scope specifies where the variable can be used and referenced. The scope of a variable defined using the let keyword is limited to the command block in which it was initially declared. In the following example, the topic variable exists only within the command block of the writeLog() function. It has no existence outside of that context.

function writeLog() {
   let topic = "closures!";
   console.log(`Let’s learn about ${topic}`);
}
writeLog(); 
// logs "Let’s learn about closures!"
console.log(`Learn more about ${topic}`); 
// Uncaught ReferenceError: topic is not defined

Often a program will involve nested levels of functions and command blocks. In those cases, the scope of a variable extends down through the chain of nested functions and blocks. The following code demonstrates this principle by which the scope of the topic variable extends into the nested() function.

function writeLog() {
   let topic = "closures!";
   nested();
   function nested() {
      console.log(`Let’s learn about ${topic}`);
   }
}
writeLog(); 
// logs "Let’s learn about closures!"

The nested() function “knows” what is meant by the topic variable because of a principle of programming called lexical scope in which the scope of variables, functions, and other objects is based on their physical location within the source code. The JavaScript Interpreter recognizes the topic variable because it is part of the larger context of the writeLog() function that encompasses the declaration of the topic variable and the creation of the nested() function.

The interpreter applies lexical scope in evaluating all variables it encounters. It starts by looking for a matching variable declaration within the current function or command block. If none is found, the interpreter moves outward to higher levels until it finds the declaration. If no declaration can be found at any level, the interpreter reports an error due to an unrecognized variable name. The entire structure of variable declarations is referred to as the lexical environment and encompasses the function and the variables which exist within that function.

 

JavaScript Closures and the Lexical Environment

The idea of the lexical environment is the key to understanding closures. To see why, examine the following code which runs the nested() function outside of the context of the writeLog() function.

function writeLog() {
   let topic = "closures!";
   function nested() {
      console.log(`Let’s learn about ${topic}`);
   }
   return nested;
}
let myClosure = writeLog();
myClosure();
// logs "Let’s learn about closures!"

When myClosure() is called in the last line of the code, it runs only the nested() function since that is what is returned by the writeLog() function. Even though the topic variable is apparently referenced outside of its lexical environment, the code still works. The reason is because of a closure that copies the lexical environment in which the topic variable existed. Closures “enclose” everything about the function, including its context within the larger source code. That is why the topic variable still had meaning when it was referenced apparently outside of the writeLog() function.

 

JavaScript Closures and Program Loops

Closures often appear in the operation of program loops that call functions at each iteration. Consider the following program loop that calls an anonymous function displaying the value of a counter variable after a 1-second delay:

for (let i = 0; i < 3; i++) {
   setTimeout(function() {
      console.log(`The counter is ${i}`);
   }, 1000);
}
// logs "The counter is 0"
// logs "The counter is 1"
// logs "The counter is 2"

The setTimeout() method delays logging anything until after the loop is finished and counter variable, i, removed from memory. Despite this, the value of the counter variable at the time the setTimeout() method was called is preserved.

The reason for this behavior is that the program loop copies and encloses the function nested within the setTimeout() method. Thus, it also copies its lexical environment. The fact that the anonymous function is not actually run until well after the loop finishes changes nothing. Because of closures, the JavaScript interpreter “remembers” the value of i at the time it was encountered in the loop.

 

JavaScript Closures and Event Handlers

Closures also appear in program loops involving event handlers that are not run until the event occurs (well after the program loop finishes). Even though the code is run later, the lexical environment surrounding the event handler function is copied because of the closure. In the following example, the index number for each hypertext link within a document is retained as part of the closure that includes the lexical environment.

let allLinks = document.links;
for (let i = 0; i < allLinks.length; i++) {
   allLinks[i].onmouseover = function() {
      console.log(`This is link ${i}`);
   }
}

When the pointer hovers over the first hypertext link, the message “This is link 0” will be logged. When the second link is hovered over the message “This is link 1” will be logged, and so forth. One important point about program loops is that the counter variable needs to be defined using the let keyword. This technique will not work if the counter variable is defined using the var keyword. This is because the lexical scope of variables defined with var is different.

When students first encounter them, closures appear magical as they appear to reference variables and values that have long since disappeared. However, we must remind students that closures are based on solid programming principles and practices. Good luck teaching and incorporating them in your programming projects and be ready to encourage your students to create their own examples once they begin searching for jobs.

 

The new JavaScript for Web Warriors 7e, is now available for students and instructors. It allows web page authors to develop interactive web pages and sites with today’s popular client-side scripting language.