119

Here is a simplified version of something I'm trying to run:

for (var i = 0; i < results.length; i++) {
    marker = results[i];
    google.maps.event.addListener(marker, 'click', function() { 
        change_selection(i);
    }); 
}

but I'm finding that every listener uses the value of results.length (the value when the for loop terminates). How can I add listeners such that each uses the value of i at the time I add it, rather than the reference to i?

MD Sayem Ahmed
  • 28,628
  • 27
  • 111
  • 178
ryan
  • 2,311
  • 3
  • 22
  • 28

6 Answers6

161

In modern browsers, you can use the let or const keywords to create a block-scoped variable:

for (let i = 0; i < results.length; i++) {
  let marker = results[i];
  google.maps.event.addListener(marker, 'click', () => change_selection(i));
}

In older browsers, you need to create a separate scope that saves the variable in its current state by passing it as a function parameter:

for (var i = 0; i < results.length; i++) {
  (function (i) {
    marker = results[i];
    google.maps.event.addListener(marker, 'click', function() { 
      change_selection(i);
    }); 
  })(i);
}

By creating an anonymous function and calling it with the variable as the first argument, you're passing-by-value to the function and creating a closure.

Andy E
  • 338,112
  • 86
  • 474
  • 445
  • 4
    You'll want to add `var` before `marker` to not pollute the global namespace. – ThiefMaster Mar 09 '11 at 14:48
  • 2
    @ThiefMaster: strangely enough, I just thought the same thing after looking at this answer for the first time in a while. However, looking at the OP's code, we can't be entirely sure that `marker` isn't already a global variable. – Andy E Mar 09 '11 at 14:50
  • having used google's map API we can safely bet that marker's scope is outside of the for loop. Nice catch Andy. – Mark Essel Apr 14 '11 at 19:28
  • I agree that this approach works, but JSLint objects to creating functions inside loops. You can create the functions outside the loop, as James Allardice shows at http://jslinterrors.com/dont-make-functions-within-a-loop – John May 14 '14 at 12:02
  • 1
    @John: one of JSLint's overzealous warnings, IMO. Adhering to Crockford's laws of writing JavaScript is completely optional, which is why I use JSHint with most warnings that assume I might not understand the code I'm writing switched off. Sadly, this is the second time in as many weeks someone's brought this up on one of my answers, but thankfully you're not so far gone as to have down voted me for it in an effort to force others to adhere to Crockford's coding ideals. ;-) – Andy E May 14 '14 at 12:18
  • @ThiefMaster Are you saying that 'declaring' `marker` without `var` (nor `const` nor `let`) will result in the OP's problem, the last value of the loop being used in all function calls? Asking because I think I observed this effect with a loop I was writing, though my function(s) were defined outside of the loop, unlike this answer. I was surprised to see that declaring my variable with `var` made such a difference. I had expected `let` to make such a difference based on [this answer](https://stackoverflow.com/a/750506/895065). – Jesse W. Collins Jul 04 '18 at 11:48
35

As well as the closures, you can use function.bind:

google.maps.event.addListener(marker, 'click', change_selection.bind(null, i));

passes the value of i in as an argument to the function when called. (null is for binding this, which you don't need in this case.)

function.bind was introduced by the Prototype framework and has been standardised in ECMAScript Fifth Edition. Until browsers all support it natively, you can add your own function.bind support using closures:

if (!('bind' in Function.prototype)) {
    Function.prototype.bind= function(owner) {
        var that= this;
        var args= Array.prototype.slice.call(arguments, 1);
        return function() {
            return that.apply(owner,
                args.length===0? arguments : arguments.length===0? args :
                args.concat(Array.prototype.slice.call(arguments, 0))
            );
        };
    };
}
bobince
  • 528,062
  • 107
  • 651
  • 834
  • 2
    Just noticed this, +1. I'm quite a fan of `bind` and can't wait for the native implementations to roll out. – Andy E Apr 18 '10 at 16:49
  • What browsers support this? Any mobile browsers? – NoBugs May 05 '12 at 07:22
  • 2
    @NoBugs: currently: IE9+. Fx4+, recent Chrome and Opera versions. Not Safari, not iPhone, Android browser has it since Ice Cream Sandwich. – bobince May 05 '12 at 19:39
13

closures:

for (var i = 0, l= results.length; i < l; i++) {
    marker = results[i];
    (function(index){
        google.maps.event.addListener(marker, 'click', function() { 
            change_selection(index);
        }); 
    })(i);
}

EDIT, 2013: These are now commonly referred to as an IIFE

David Murdoch
  • 87,823
  • 39
  • 148
  • 191
  • Nothing *wrong* here, but -1 just because Andy E got there first with more explanation; this answer doesn't add anything to the page as it stands. – Mark Amery Dec 21 '14 at 15:20
  • 4
    I'm not sure you understand the reasons for downvoting. And this answer does add information in addition to Andy's (excellent) answer: IIFE. – David Murdoch Dec 21 '14 at 15:33
2

You're winding up with a closure. Here's an article on closures and how to work with them. Check out Example 5 on the page; that's the scenario you're dealing with.

EDIT: Four years later, that link is dead. The root of the issue above is that the for loop forms closures (specifically on marker = results[i]). As marker is passed into addEventListener, you see the side effect of the closure: the shared "environment" is updated with each iteration of the loop, before it's finally "saved" via the closure after the final iteration. MDN explains this very well.

ajm
  • 19,795
  • 3
  • 32
  • 37
-2
for (var i = 0; i < results.length; i++) {
    marker = results[i];
    google.maps.event.addListener(marker, 'click', (function(i) {
        return function(){
            change_selection(i);
        }
    })(i)); 
}
-4

I think we can define a temporary variable to store the value of i.

for (var i = 0; i < results.length; i++) {
 var marker = results[i];
 var j = i;
 google.maps.event.addListener(marker, 'click', function() { 
   change_selection(j);
 }); 
}

I haven't tested it though.

Ken
  • 1
  • 5
    The reason this won't work is that JavaScript lacks block-level scoping. All scoping is function-level. You can only create a new scope by calling a function, which is what we see in the other answers. Without calling a function for each iteration of the loop, there is no way to provide a different closure to each map-event-listener callback. This is a problem that's solved transparently for you whenever you use an iteration-helper like `$.each()` or `_.each()`. – Keen Apr 05 '13 at 11:57