0

I'm trying to figure out why the standard for loop doesn't work in this case but a for of loop works. The problem with the simple for loop is that, if you open the console, it returns an error of class list undefined and therefore doesn't add the 'open' class to the divs - see the two code snippets below:

Code with working for of loop:

if (document.querySelectorAll('.wrapper').length) {
  var els = document.querySelectorAll('.wrapper');

  for (var el of els) {
    var toggler = el.querySelector('a');

    toggler.addEventListener('click', function(e) {
      e.preventDefault();
      console.log(e.target);
      el.classList.toggle('open');
      e.target.classList.toggle('open');
    });
  }
}
<div class='wrapper'>
  <a href='#'>click me!</a>
</div>

Code with standard for loop (not working):

if (document.querySelectorAll('.wrapper').length) {
  var els = document.querySelectorAll('.wrapper');

  for (var i = 0; i < els.length; i++) {
    var toggler = els[i].querySelector('a');

    toggler.addEventListener('click', function(e) {
      e.preventDefault();
      console.log(e.target);
      els[i].classList.toggle('open');
      e.target.classList.toggle('open');
    });
  }
}
<div class='wrapper'>
  <a href='#'>click me!</a>
</div>

Can someone explain why the standard for loop fails but the for of loop works? And how can this work using a standard for loop? Thanks for any help here.

2 Answers 2

1

This has to do with how var is hoisted outside of the event listener and gets redefined during the loop, but does not stay inside the event listener's scope. Changing to modern const and let variable declaration will fix your issue.

const els = document.querySelectorAll('.wrapper');

if (els.length) {
  for (let i = 0; i < els.length; i++) {
    const toggler = els[i].querySelector('a');

    toggler.addEventListener('click', function(e) {
      e.preventDefault();
      console.log(e.target);
      els[i].classList.toggle('open');
      e.target.classList.toggle('open');
    });
  }
}
<div class='wrapper'>
  <a href='#'>click me!</a>
</div>


If your installation is old enough to not allow for let or const, you could try this instead:

var els = document.querySelectorAll('.wrapper');
if (els.length) {
  for (var i = 0; i < els.length; i++) {
    var el = els[i]; // Re-declare `el` inside the loop
    var toggler = el.querySelector('a');

    toggler.addEventListener('click', function(e) {
      e.preventDefault();
      console.log(e.target);
      el.classList.toggle('open');
      e.target.classList.toggle('open');
    });
  }
}
<div class='wrapper'>
  <a href='#'>click me!</a>
</div>

Sign up to request clarification or add additional context in comments.

3 Comments

thanks for the useful info, however, it needs to work without being able to use const or let keywords due to our setup here (annoying I know). Is this achievable?
also, how does the els[i] get redefined during the event listener? It was defined before this and is the scope above the event listener, so theoretically should be definable?
@user8758206 Basically when you declare i, it's declaration is hoisted up to the top of the function that the for loop is in, and as such does not actually have a scope locked to inside the loop. So, when it gets incremented at the end of the loop (which then breaks out of the loop, but i is still incremented), the i in the els[i] inside the event listener is also incremented. Read up on hoisting here.
0

I know there's an accepted answer but would give my insight anyway, it took me more than an hour to google, test and write this answer. It focuses on seeking for the how and why, ended up relating to hoisting and closures. And in the process of writing this, I was getting the point that "fuck, I don't know JS yet, maybe the answer I'll be giving will also not be the correct one". If that's the case, PLEASE, correct me.


You can diagnose the problem by adding these 3 console.log and see the different behaviors between var and let.

if (document.querySelectorAll('.wrapper').length) {
  var els = document.querySelectorAll('.wrapper');

  for (var/*alternate with let*/ i = 0; i < els.length; i++) {
        var toggler = els[i].querySelector('a');

        toggler.addEventListener('click', function(e) {
            e.preventDefault();
            console.log(e.target);
            console.log('i', i) // var: 1, let: 0
            console.log('els[0]', els[0]) // both are the element `.wrapper a`
            els[i].classList.toggle('open');
            e.target.classList.toggle('open');
        });
  }
}
console.log('load', i) // var: 1, let: ReferenceError

The result will be:

var

  • log 1 when the page first loads
  • log 1 every click

let

  • Uncaught ReferenceError: i is not defined when the page first loads
  • log 0 every click, and also your .wrapper a item as expected.

In both cases, els[0] is a valid item.


Now connect the dots, you got Cannot read property 'classList' of undefined means what sits before .classList (els[i]) is undefined. But els[0] is not undefined, so the one undefined was els[1] is undefined because els only has 1 element els[0].

So why, why in the case of var, i is 1, but with let, i is 0? And when reference it at the bottom, with var i is 1, but with let, you can't reference it (Reference Error). That's called hoisting, to better understand the specific example that most related to this post, you could take a look at the last example of this.

If you add another console.log(window.i) at the bottom after console.log('load', i), you'll see that i is equal to window.i, and it's i being alive the whole time. So when you call els[i] in the event handler, i will reference to the i of the function context of toggler.addEventListener('click', function(e) {, it's related to another term called closures. In the case of var, the i of your event handler is in the same reference (means they are the same chunk of memory on RAM) with the i of window.i, and it's still connected the whole time, and i was being incremented to 1 after breaking the loop. So after the loop, window.i is 1, the i in the function is also 1. On the other hand, in the case of let, i has the scope of the for loop (because let has the block scope), so it dies after the loop finished, it disconnects with the i in els[i] of the event handler before being referenced by els[i], before the new value after increment get updated to els[i], so i in the context of the handler is now isolated with the i whose value of 1. That's why i got the correct value with let.

But btw, the i of the function handler is still alive, if you try changing the value of i in the handler, you'll see that it'll affect the value the next time invoked.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.