0

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!

Dwix
  • 1,139
  • 3
  • 20
  • 45
  • 1
    But.... you don't have a closure? – adeneo Nov 02 '16 at 20:59
  • @adeneo I changed it so now it looks more like it – Dwix Nov 02 '16 at 21:02
  • 1
    @dwix still not a closure, though... The problem is that you need one _inside_ the loop. Have you looked at this? http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example?rq=1 – VLAZ Nov 02 '16 at 21:04
  • If you already know the term "closure", it's hard to understand how you couldn't find the answer. –  Nov 02 '16 at 21:05
  • @vlaz A closure occurs ANY time you have nested functions. There is most definitely a closure here since the callback function is defined within the `func` function. – Scott Marcus Nov 02 '16 at 21:30
  • @ScottMarcus there is a closure but not the one that OP needs. It's quite literally useless to wrap the entire code in a function as _that_ scope is not the important one to preserve. If you are unclear on why that is, follow the link I provided - it's why I put it there. – VLAZ Nov 02 '16 at 21:35
  • @vlaz That's not the point. You said he didn't have a closure in the code he posted. He does. – Scott Marcus Nov 02 '16 at 21:48
  • @ScottMarcus and I pointed at the resource that explains where the closure needs to be put and why. If you feel like nitpicking a comment, which is supposed to be, you know, _short_, for not being as detailed as the answer it's pointing to, then I don't know what to tell you. Perhaps you need a better hobby. – VLAZ Nov 02 '16 at 21:51
  • @vlaz I don't think it's nitpicking to correct an incorrect statement. There is a closure. Putting a link to a resource that explains where it needs to be has zero to do with you incorrectly telling the OP that he doesn't have something that he does. Additionally, moving the closure is only one way of solving the problem. In this case, it's better to remove the reliance on the free variable in the first place. – Scott Marcus Nov 02 '16 at 21:54
  • @ScottMarcus so, explain something to me - is the function `func` a closure inside the for loop? Because I am under the impression it isn't. – VLAZ Nov 02 '16 at 21:58
  • @vlaz The `func` function closes around each of the functions that are assigned as paragraph click callbacks via an anonymous function. Simply put, closures occur ANY time there is a parent/child function relationship. Whether any side effects occur depend on whether the child function(s) use free variables lexically scoped to the parent AND those functions will outlive their parents (because they are returned or stored in a longer lived object). – Scott Marcus Nov 02 '16 at 22:00
  • @ScottMarcus right, so you are saying that it closes AROUND the for loop, therefore, it is not _inside_ the for loop correct? Tell me now - does the title specify a closure is INSIDE or OUTSIDE the for loop? – VLAZ Nov 02 '16 at 22:04
  • @vlaz No. Closures don't close around anything other than other functions. Not loops. The callback function is INSIDE the loop and this is the root of the problem. Everyone seems to get this but you. – Scott Marcus Nov 02 '16 at 22:08
  • @ScottMarcus nice, insulting my intelligence. When you could not answer a simple question I repeated twice for you. Let me answer it for you: the `func` function is _not_ inside the for loop. It is _outside_ the for loop. When OP suggested in the title "a closure inside the for loop" adeneo pointed out that wasn't the case. I don't see you refuting his statement. Later, OP edited in the `func` which, let me remind you _again_ is not inside. Which is when I pointed out the construct OP wanted still wasn't there and pointed at the resource that clarified it. Stop insulting my intelligence, plox. – VLAZ Nov 02 '16 at 22:22
  • @vlaz What I'm doing is telling you that inside/outside is not the point of the question despite the title. The OP doesn't have enough knowledge here about closures to accurately write the title. The POINT of the post is obvious. You continue to get hung up on something that has zero to do with the entire point. A closure exists and the loop is not functioning as the OP expected. Telling the OP that he doesn't have a closure, when he does doesn't help anyone and is just plain wrong. Sorry, if you can't see that. – Scott Marcus Nov 02 '16 at 22:49

4 Answers4

2

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>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • I'm an absolute beginner, I thought at Line 5, the callback is executed and meant that we're just attaching a Listener to the current paragraph, So even if we don't click on any of them, they all have a Listener attached with a difference `i` value for each paragraph. Thank you ! – Dwix Nov 02 '16 at 21:14
  • @dwix No, the callback is being assigned on line 5, it's not executed until later, when the human clicks on one of the paragraphs. By then, the loop is done and all callbacks are pointing at the LAST `i` value, which is why no matter which paragraph you click, the last one is affected. Check out the code snippet I just added which shows a simple solution. – Scott Marcus Nov 02 '16 at 21:20
  • Thanks a ton, now I understand! – Dwix Nov 02 '16 at 21:22
1

A closure is a function that closes in the values of the variables, so they don't change on the next iteration of the loop, which happens before the click happens etc.

var paragraphs = document.querySelectorAll('p');

for (var i = 0; i < paragraphs.length; i++) {

    (function(p) { // <- any function call, would create a new scope, and hence a closure
        p.addEventListener('click', function() {
            p.classList.toggle('red'); // note that "this" would work here, instead of "p"
        });
    })(paragraphs[i]); // <- in this case, it's an IIFE, but it doesn't have to be

}

That would be a closure

adeneo
  • 312,895
  • 29
  • 395
  • 388
  • I know an IIFE or `this` can fix it, I just need to understand the logic, so the `function() { p.classList.toggle('red') }` is not a closure? is it called a callback? And why doesn't it capture the value of `i` anyways at each iteration ? – Dwix Nov 02 '16 at 21:08
  • 1
    @dwix it technically _is_ a closure, since it's using the value of `p` from outside its scope. Unfortunately that variable no longer has the value it's supposed to have by the time the callback is invoked. – Alnitak Nov 02 '16 at 21:09
  • 1
    Tha callback function for the event handler is a closure, but it doesn't create a new variable holding the element, as Alnitak noted it simply gets `p` from the outer scope, and as the function only runs when someone clicks, that outer `p` has already changed to the last element iterated over. – adeneo Nov 02 '16 at 21:11
  • 1
    The IIFE above, or even `array.forEach(item) {...` creates a **new** variable from the parameter, and therefore encloses that value in the new scope, so it doesn't change – adeneo Nov 02 '16 at 21:13
1

In JavaScript (ECMA-Script 5 and earlier versions), only functions can create scopes.

In the other hand, closures don't capture variable values. That is, you need to do it yourself as you've already said using IIFEs (immediately-invoked function expressions):

for (var i = 0; i < paragraphs.length; i++) {
  (function(p) {
    p.addEventListener('click', function() {
      p.classList.toggle('red')
    })
  })(paragraphs[i]);
}

BTW who knows if you can simplify this code using document.querySelectorAll:

// Now you don't need IIFEs anymore...
// Change the "p" selector with whatever CSS selector
// that might fit better in your scenario...
Array.from(document.querySelectorAll("p"))
  .forEach(function(p) {
    p.addEventListener("click", function() {
      p.classList.toggle('red');
    });
  });

Well, in fact, you can refactor your code to use Array.prototype.forEach and you won't need to use IIFEs too:

paragraphs.forEach(function(p) {
    p.addEventListener("click", function() {
      p.classList.toggle('red');
    });
});
Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
  • Thank you, I get it now. – Dwix Nov 02 '16 at 21:19
  • 1
    @dwix I'm glad you got it, it's very important to master JavaScript – Matías Fidemraizer Nov 02 '16 at 21:23
  • I was working with Java/JEE for the past year, and I just find some JS stuff very different and confusing for beginners. Thanks again. – Dwix Nov 02 '16 at 21:30
  • 1
    @dwix JavaScript is a very different language. It lacks classes while functions are the first citizen construct... ES2015 and above have classes but they're syntactic sugar over prototypes and prototype chain... Welcome to the mysterious and wonderful world of JavaScript ;D – Matías Fidemraizer Nov 02 '16 at 21:34
1

This is the classic problem caused by your variable p being bound to a variable whose value has changed by the time the callback is invoked.

For iterating over an array and calling async code without any special tricks you can simply use Array.prototype.forEach (and on some browsers the classList property also supports this method):

paragraphs.forEach(function(p) {
    p.addEventListener('click', function() {
         p.classList.toggle('red');
    });
});

Since p is a bound parameter of the forEach callback it always holds the expected value for the current iteration, and the correct element with toggle.

If your browser doesn't support classList.forEach then use:

[].prototype.forEach.call(paragraphs, ...);
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • 1
    Unfortunately, a nodeList gotten with `querySelectorAll` does not support `forEach` – adeneo Nov 02 '16 at 21:11
  • So the callback gets executed only and only at the moment of `click` on a given paragraph ? – Dwix Nov 02 '16 at 21:16
  • @adeneo it does on Chrome and is part of the `DOMTokenList` interface, but the "simple" alternative is `[].prototype.forEach.call(paragraphs, ...)` – Alnitak Nov 02 '16 at 21:18
  • I did not know that was supported now for `querySelectorAll`, that's cool, finally. – adeneo Nov 02 '16 at 22:41