dwix dwix - 1 month ago 10
Javascript Question

Closure inside a for Loop in Javascript with confusing results

I'm a beginner in JavaScript, I've read every answer in similar topics, I still can't understand what's really going on because no one explained the part that I'm confused about.

I have a HTML document with two paragraphs, and this is what I'm using to change the color of a paragraph to

red
when I click on it :

var func = function() {
/*Line 1*/ var paragraphs = document.querySelectorAll('p')
/*Line 2*/
/*Line 3*/ for (var i = 0; i < paragraphs.length; i++) {
/*Line 4*/ p = paragraphs[i]
/*Line 5*/ p.addEventListener('click', function() {
/*Line 6*/ p.classList.toggle('red')
/*Line 7*/ })
/*Line 8*/ }
}
func();


The result is, where ever I click, only the last paragraph changes color to red. All the answers to questions similar to mine said that after the
For
loop is finished, the value of
i
will be
1
, so that's what the closure will use, and then the
eventListener
will be added to same second paragraph ? And that I can fix this using a
Immediately-Invoked Function
or
let
to make
i
private to the closure, I don't know why should I make
ì
private if the closure has access to it at each iteration of the loop..

I just can't understand what's really going on here, isn't the loop executed line after another? At the start
i
will have a value of
0
, so at
Line 4
the variable
p
will have the first paragraph, then at
Line 5-6
the function will use that
p
and attach the listener to it, then the loop gets executed a second time and
i
will have a value of
1
, then at
Line 5-6
again the closure gets the new value of
p
?

I know the closure have access to the global variables here like
i
, so it have access to the
p
at
Line 4
when its value changes.

What am I missing here please? Thank you very much in advance!

Answer

You are showing the proverbial closure example....

This can be difficult to grasp at first

/*Line 1*/ var paragraphs = document.querySelectorAll('p')
/*Line 2*/
/*Line 3*/ for (var i = 0; i < paragraphs.length; i++) {
/*Line 4*/    p = paragraphs[i]
/*Line 5*/    p.addEventListener('click', function() {
/*Line 6*/      p.classList.toggle('red')
/*Line 7*/    })
/*Line 8*/ }

Lines 6 and 7 contain the anonymous callback function that is being stored in each paragraph. That function relies on the variable i from the parent function (you didn't show that one in your code, but we get it) because the inner function uses p which is defined as paragraphs[i]. So, even though you aren't using i explicitly in the inner function, your p variable is. Let's say there are 7 paragraphs in the document... Because of this, you have 7 functions that are "closed" around the one i variable. i cannot go out of scope when the parent function terminates because the 7 functions need it. So, by the time a human comes along to click on one of the paragraphs, the loop has completed (i will now be 8) and each function is looking at the same i value.

To solve the problem, the click callback functions need to each get their own value, rather than share one. This can be accomplished in several ways, but they all involve PASSING a copy of i into the click callback function so that a COPY of the i value will be store in each of the click callback functions. There will still be a closure, but there won't be the side effects you initially encountered because the nested functions won't be relying on variables from a parent function.

Here's an example that removes i from the nested function, thus solving the problem:

var paragraphs = document.querySelectorAll('p')

for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].addEventListener('click', function() {
      this.classList.toggle('red')
    });
}
.red {color:red;}
<p>Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>
<p class="red">Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>