Johnny
Johnny

Reputation: 161

Variable out of scope when applying event listener

Why is i out of scope in the callback function in this code?

// All menu items collection
var menuItems = document.getElementsByClassName('menu-item');

// Loop trough all menu items and attach
// event listeners.
for (var i = 0; i < menuItems.length; i++) {
        // Check if element truely exsists
        if (menuItems[i]) {
            menuItems[i].addEventListener('click', function(e){
                ////////////////////////////////
                // NOTE: i, is out of scope! //
                //////////////////////////////
                var icon = menuItems[i].children[1],
                    submenu = menuItems[i].children[2];

                // Change icon color
                if (icon.style.background !== "blue") {
                    icon.style.background = "blue";
                } else {
                    icon.style.background = "red";         
                }

                // Show/hide submenu
                if (submenu.style.display !== "block") {
                    submenu.style.display = "block";
                } else {
                    submenu.style.display = "none";         
                }
            });
        }
}

Upvotes: 2

Views: 3356

Answers (1)

Guffa
Guffa

Reputation: 700192

The variable i is not out of scope. It's just that the event handler will be called some time after the loop has completed, so the variable i has a value that points outside the menuItems array.

Wrap the code in a function, to create a scope where you have a copy of the variable for each iteration:

for (var i = 0; i < menuItems.length; i++) {
  // Check if element truely exsists
  if (menuItems[i]) {

    (function(i){  

      menuItems[i].addEventListener('click', function(e){
        var icon = menuItems[i].children[1],
            submenu = menuItems[i].children[2];

        // Change icon color
        if (icon.style.background !== "blue") {
          icon.style.background = "blue";
        } else {
          icon.style.background = "red";         
        }

        // Show/hide submenu
        if (submenu.style.display !== "block") {
          submenu.style.display = "block";
        } else {
          submenu.style.display = "none";         
        }
      });

    })(i);

  }
}

As the event handler uses the i variable, it will be captured in a closure object for that function. Each event handler will have a separate closure object, but without the function wrapper to create a separate i variable for each iteration, all the closures would capture the same variable.

The closure for a function contains all local variables that the function uses from the outer scope. Unless menuItems is a global variable, it will also be in the closure.

Upvotes: 5

Related Questions