120

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?

0

6 Answers 6

162

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.

Sign up to request clarification or add additional context in comments.

6 Comments

You'll want to add var before marker to not pollute the global namespace.
@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.
having used google's map API we can safely bet that marker's scope is outside of the for loop. Nice catch Andy.
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 jslinterrors.com/dont-make-functions-within-a-loop
@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. ;-)
|
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))
            );
        };
    };
}

3 Comments

Just noticed this, +1. I'm quite a fan of bind and can't wait for the native implementations to roll out.
What browsers support this? Any mobile browsers?
@NoBugs: currently: IE9+. Fx4+, recent Chrome and Opera versions. Not Safari, not iPhone, Android browser has it since Ice Cream Sandwich.
14

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

2 Comments

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.
I'm not sure you understand the reasons for downvoting. And this answer does add information in addition to Andy's (excellent) answer: IIFE.
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.

Comments

-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)); 
}

1 Comment

this would be a better answer if you explained why it works.
-3

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.

1 Comment

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().

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.