Rob
Rob

Reputation: 11

Closures, Loops, and Event Handlers

I have a fundamental misunderstanding of closures and perhaps looping statements and event handlers. It stems from the following snippet of code which is often used to demonstrate a misuse of closures:

var myNodes = document.getElementsByTagName('li');
for (var i = 0; i < myNodes.length; i++) {
    myNodes[i].addEventListener('click', function(){ console.log(i); });
}

It's usually explained that this code doesn't work because each iteration of the loop creates a new event handler, and each new event handler is a closure. This is the part I don't understand. I don't see how this produces closures. A closure is defined by a function being declared within an outer function, and the outer function publicizing the inner function. Where is that pattern here?

Lastly, I don't even understand why the event handlers being closures is relevant. Why do these closures seem to wait until the loop is complete to capture the environment variables? Why not when the loop is first initiated? Why not capture them as expected, with each iteration of the loop?

Upvotes: 1

Views: 101

Answers (3)

traktor
traktor

Reputation: 19311

A closure is defined by a function being declared within an outer function, and the outer function publicizing the inner function. Where is that pattern here

"publicizing` is an alternative way of saying that a reference to a nested function is reachable in code after its outer function has returned. Variables of the outer function (and its outer functions if there are any) are within the scope chain of the inner function and not discarded after the outer function executes and returns. Notionally a "closure" is this variable space held in a scope chain which can not be accessed by code outside the function. The nested function is also sometimes referred to as a "closure" by association.

In the posted example, a new anonymous functions is passed to addEventListener each time it is called. Assuming the for loop is part of an initialization function, registering the functions as handlers effectively "publishes" them. Because references to the handler function objects are retained in browser DOM support code, their scope chain variables are prevented from being garbage collected and can be said to form closures.

Why do these closures seem to wait until the loop is complete to capture the environment variables?

Javascript is single threaded: executing script does not get interrupted by events which are queued for processing after the thread completes. This means the for loop completes execution, leaving i set to myNodes.length, before any event handling is processed by the browser. So each click handler, when executed, will access the same i variable in outer scope, and will log myNodes.length to the console.

Upvotes: 0

jmcgriz
jmcgriz

Reputation: 3358

The issue is that a for loop doesn't create a separate closure with each loop through. Because of that, it's suggested to avoid declaring variables inline like that, since it's misleading.

The more accurate version of that loop is shown below.

The way around it is shown afterward; by passing the value of i to a separate function that assigns the click handler, you're creating a local scope inside of that function instance that preserves the value.

var myNodes = document.getElementsByTagName('li');
var i;

for (i = 0; i < myNodes.length; i++) {
  myNodes[i].addEventListener('click', function() {
    console.log('From First Loop', i);
  });
}
console.log('After loop, i =', i)

//Here's the way around it, creating a handler function
function bindClick(index){
  myNodes[index].addEventListener('click', function() {
    console.log('From Handler', index);
  });
}

for (i = 0; i < myNodes.length; i++) {
  bindClick(i);
}
<ul>
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
</ul>

Upvotes: 1

DanielS
DanielS

Reputation: 774

Let's say we have 5 nodes.

And let's assume we'll click on any node after the loop finished.

If no closures were involved, when clicking on myNodes[3] you'd expect this code to write 3 to console.

But, that's not what it's going to print. It'll print the last value of i (4 in this case).

This is happening because when you declare function(){ console.log(i);, you capture the i variable from the outer block (your loop). In time of the declaration i will have the "expected" value identical to the value in the loop. But, when you actually click on the node, the code in the loop already ran. The value of i at this point is 4 and that's the value inner function will print to log.

Upvotes: 1

Related Questions