Search code examples
javascriptfor-loopclosuresiife

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!


Solution

  • 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 5, 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 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 or removing the use of i alltogether. 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>