0

I have this weird situation and I don't know why do the output is not what I expect. This is only a simple for-loop function. Can somebody explain me why this happens?

var pm = 2; 
for (var i = 0; i < pm; i++) {
     $("#specialty_pm_"+i).mouseenter(function(){
            alert(i);
    });
};

I have 2 divs in my html that has id="specialty_pm_<?php echo $countPM; ?>" and that div is inside a for loop function in php. foreach ($employee_info as $emp_info){ $countPM++; } I expect that the alert on hover in the 1st div is '1' and the 2nd div is '2'. but when I hover the 1st div, it will alert '2'.

user2864740
  • 60,010
  • 15
  • 145
  • 220
Vincent815
  • 139
  • 1
  • 2
  • 9

4 Answers4

3

you should use JavaScript closure:

var pm = 2; 
for (var i = 0; i < pm; i++) {
    var func = (function(i){
        return function(){
            alert(i);
        }
    })(i);
    $("#specialty_pm_"+i).mouseenter(func);
};

The point is in your code all the mouseenter functions use the same variable i, and after your loop ends, it has its last value which is 2. Using a scope chain, with nested functions and their closures in JavaScript, you can create safe scopes for your variables. Basically what nested functions do, is to provide a outer LexicalEnvironment for the inner function. You can find more information in this post:

Scope Chain in Javascript.

Community
  • 1
  • 1
Mehran Hatami
  • 12,723
  • 6
  • 28
  • 35
  • 2
    *"Using closures in JavaScript you can create safe scopes for your variables."* No no no! The way closures work in JS is the **reason** for this problem in the first place. The event handlers that are created in the loop are *already* closures. Closures themselves don't create scope, only *function invocation* creates a new scope. And that's the solution to the problem: Execute a function to create a new scope. Whether that function is a closure or not doesn't matter. – Felix Kling Mar 01 '14 at 20:22
  • 1
    @FelixKling: My bad in describing the whole concept and thanks for your informative feedback. I try to be more descriptive. – Mehran Hatami Mar 01 '14 at 20:32
2

You alert can't wirks because i has only one instance. Yoi can check variable i inside your div in this case.

try this:

$("#specialty_pm_"+i).mouseenter(function(){
     var id = $(this).attr('id');
     alert(id.substring(0,13));
});
Alessandro Minoccheri
  • 35,521
  • 22
  • 122
  • 171
1

The reason, as already mentioned, is that the scope of i is the same for both eventhandlers, and as such it will have the same value for both of them.

There is a couple of solution for this problem.

Solution 1: create a new scope via a immediate function

var pm = 2; 
for (var i = 0; i < pm; i++) {
     $("#specialty_pm_"+i).mouseenter(function(instance){
        return function() {  alert(instance); };
    }(i));
};

You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/

Solution 2: use jQuerys data method to store the value

var pm = 2; 
for (var i = 0; i < pm; i++) {
     $("#specialty_pm_"+i).data('instance',i).mouseenter(function(){
            alert($(this).data('instance'));
    });
};

You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/1/

Solution 3: bind the instance number to the eventhandler

var pm = 2; 
for (var i = 0; i < pm; i++) {
     $("#specialty_pm_"+i).mouseenter(function(instance){
        alert(instance);
    }.bind(null,i));
};

You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/2/

Solution 3 has a few caveats - this is being bound as null, and thus it can no longer be used as a reference to the dom element, like jQuery eventhandlers nomrally do. Also bind isn't supported by older browsers, but this can be mitigated by usinga polyfill, a good one can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

Solution 4: Be smart and use a delegate instead of binding event handlers in a loop

     $(document.body).on('mouseenter','.specialty_pm',function(){
        alert($(this).data('id'));
    });

You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/4/

Solution 4 is the "right way" to do it, but it will require you to change the way you build your markup

Martin Jespersen
  • 25,743
  • 8
  • 56
  • 68
0

You could get rid of the for loop in your JavaScript and use jQuery's Starts With Selector to select ALL elements whose id begins with 'specialty_pm_'

$("[id^='specialty_pm_']").on("mouseenter",function(){
     var id = $(this).attr('id');
     alert(id.substring(13));
});

E.G: http://jsfiddle.net/6bTJ3/

Alternatively you could add a class to each of these specialty_pm_[n] items to make it even simpler to select them in jQuery.

Moob
  • 14,420
  • 1
  • 34
  • 47