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.