0

I have the following code that adds an onmouseover event to a bullet onload

for (var i = 0; i <= 3; i++) {
    document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
}

This is the function that it is calling. It is supposed to add a css class to the menu item as the mouse goes over it.

function addBarOnHover(node) {
    document.getElementById('menu').getElementsByTagName('li')[node].className = "current_page_item"; }

When the function is called, I keep getting the error:

"document.getElementById("menu").getElementsByTagName("li")[node] is undefined"

The thing that is stumping me is I added an alert(node) statement to the addBarOnHover function to see what the value of the parameter was. The alert said the value of the parameter being passed was 4. I'm not sure how this could happen with the loop I have set up.

Any help would be much appreciated.

Jack
  • 10,943
  • 13
  • 50
  • 65
chuckw87
  • 647
  • 1
  • 6
  • 14
  • possible duplicate of [Javascript closure inside loops - simple practical example](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Felix Kling Nov 15 '12 at 03:30

2 Answers2

4

This is a common problem when you close over an iteration variable. Wrap the for body in an extra method to capture the value of the iteration variable:

for (var i = 0; i <= 3; i++) {
  (function(i){ //here
    document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
  })(i); //here
}

an anonymous function is created each time the loop is entered, and it is passed the current value of the iteration variable. i inside the anonymous function refers to the argument of this function, rather than the i in the outer scope.

You could also rename the inner variable for clarity:

for(var i=0; i<=3; i++){
  (function(ii){
    //use ii as i
  })(i)
}

Without capturing the iteration variable, the value of i when it is finally used in the anonymous handler has been already changed to 4. There's only one i in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead.

John Dvorak
  • 26,799
  • 13
  • 69
  • 83
  • I don't get it. How does this happen/? – SoWhat Nov 15 '12 at 03:25
  • @SomeshMukherjee That's because the value of `i` when it is finally used in the anonymous handler has been already changed to `4`. There's only one `i` in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead. – John Dvorak Nov 15 '12 at 03:34
  • 1
    @Somesh: Think about variables as pointers to memory. In closures you just have that pointer, the value changed in the meantime though. When you *call* a function and *pass* values to it, then a copy of that value is generated. – Felix Kling Nov 15 '12 at 03:53
1

i is being passed as a reference (not by value), so once the onmouseover callback is called, the value of i has already become 4.

You'll have to create your callback function using another function:

var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('li');

for (var i = 0; i <= 3; i++) {
    items[i].onmouseover = (function(i) {
        return function() {
            addBarOnHover(i);
        };
    })(i);
}

You could make it a little more readable by making a helper function:

var createCallback = function(i) {
    return function() {
        addBarOnHover(i);
    };
};

for (var i = 0; i <= 3; i++) {
    items[i].onmouseover = createCallback(i);
}
Blender
  • 289,723
  • 53
  • 439
  • 496
  • +1 for the explanation but I don't think this pattern is very readable. – John Dvorak Nov 15 '12 at 03:25
  • @JanDvorak: I *think* my edit takes care of that. I have a feeling that it might suffer from the same problem as the original code. – Blender Nov 15 '12 at 03:27
  • I still prefer the return-less version. It's shorter (by the one keyword) and it could be promoted into a common pattern (note that if multiple handlers are created in the same loop, the returnless version reuses the capturing scope whereas this solution will create one scope per handler) – John Dvorak Nov 15 '12 at 03:42
  • @JanDvorak: Huh. I thought the `return` was necessary. So all you need to do is just pass `i` through into another scope? – Blender Nov 15 '12 at 03:44
  • See my answer. I have seen this pattern on a blog discussing "immediately invoked function expression"s. There's no need to return anything - just capture the value with an argument to an IIFE. – John Dvorak Nov 15 '12 at 03:47
  • @JanDvorak: I'll have to read up on this a little more. Why does the `i` in your anonymous function not refer to the same `i` that's outside of the scope? Or does the `i` in the scope of the anonymous function take precedence over the `i` in the parent scope and effectively "replace" it? – Blender Nov 15 '12 at 03:50
  • Variables in the inner scope always take precedence over the variables in the outer scope, and arguments to a function within the function's scope. Note that `i` is declared as an argument to the anonymous function and _shadows_ the iteration variable itself. A variable is shadowed by its value - and that's what I like about it. – John Dvorak Nov 15 '12 at 03:55
  • @JanDvorak: Okay, that's what I didn't know. I for some reason thought that the same `i` passed all the way through both scopes. That's pretty neat. – Blender Nov 15 '12 at 03:58
  • The same shadowing happens in your case, except in your case, the function that closes over the shadow is returned from its scope (which forces the scope to end) before being used. – John Dvorak Nov 15 '12 at 03:59
  • In the second case, there's no shadowing because the defining scope of `createCallback` has no access to the iteration variable, only to its value as its own argument. – John Dvorak Nov 15 '12 at 04:01
  • @JanDvorak: Thanks for the help! I'll have to read up on passing things by reference and by value in other languages. I learned just enough C++ to confuse me. – Blender Nov 15 '12 at 04:03
  • Javascript does not have pass-by-reference for primitive types, only closures (which act like implicit references being passed around). – John Dvorak Nov 15 '12 at 04:05