Javascript variable scope inside for loop - javascript

How do I maintain access to the i variable inside my for loop below? I'm trying to learn, not just get the answer, so a bit of explanation would be very helpful. Thank you!
var el,
len = statesPolyStrings.length;
for (var i = 0; i < len; i++) {
el = document.getElementById(statesPolyStrings[i]);
google.maps.event.addDomListener(el, 'mouseover', function() {
$("#"+statesPolyStrings[i]).addClass("highlight");
statesPolyObjects[i].setOptions({ strokeWeight: '2' });
});
}

All of your callbacks share the same i variable.
When the event handler actually runs, i is after the end of the array.
You need to wrap the loop body in a self-invoking function that takes i as a parameter.
This way, each iteration will get its own, unchanging, i variable.
For example:
for (var i = 0; i < statesPolyStrings.length; i++) {
(function(i) {
...
})(i);
}

for (var i = 0; i < statesPolyStrings.length; i++) {
(function(i){
google.maps.event.addDomListener(document.getElementById(statesPolyStrings[i]), 'mouseover', function() {
$("#"+statesPolyStrings[i]).addClass("highlight");
statesPolyObjects[i].setOptions({ strokeWeight: '2' });
});
})(i)
}

The trick with self-invoking functions works fine: it creates a new scope (maybe google for 'scope in functions javascript') and therefore handles i as a different variable and delivers the right value to your event listener callback function.
But you actually don't need to find your element again with jQuery as you already assigned an event listener to it and inside your function you have a reference to your element with this.
And as you're using jQuery anyhow, it's then easy to find the correct index (your i) of statesPolyObjects with $.inArray() passing the id of your element and the statesPolyStrings array (assuming you're dealing with unique IDs. If not, $("#"+statesPolyStrings[i]) would also fail, as it takes the first it finds).
var el;
for (var i = 0, len = statesPolyStrings.length; i < len; i++) {
el = document.getElementById(statesPolyStrings[i]);
google.maps.event.addDomListener(el, 'mouseover', function(event) {
$(this).addClass("highlight");
statesPolyObjects[$.inArray(this.id, statesPolyStrings)].
setOptions({ strokeWeight: '2' });
});
}
If you still want to stick with the self-invoking function you should anyhow change the following line:
("#"+statesPolyStrings[i]).addClass("highlight");
to
$(this).addClass("highlight");
If you're not familiar enough with this and events you might want to read this article:
http://www.sitepoint.com/javascript-this-event-handlers/
You might have noticed that I also wrote the argument event inside the anonymous callback function. Try to console.log this event you get delivered for free with any event listener callback function and explore all the other things you have access to. For example, you can find the actual element you clicked on with event.target (as the actual mouseover might have happened to a child of your element). So:
google.maps.event.addDomListener(el, 'mouseover', function(event) {
console.log(event);
...
and open the console of your browser to see what event delivers...
Be aware though that google.maps.event.addDomListener passes something different then document.body.addEventListener and there is also a difference between browsers. jQuery.on() for example also delivers some different things in the event object, but there you can at least count on the same data in all browsers.

Related

Why is for loop setting all jquery onclick to last iteration [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 5 years ago.
I have the following code that works
var btns = $('.gotobtn');
$('#'+btns.get(0).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[0]); });
$('#'+btns.get(1).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[1]); });
$('#'+btns.get(2).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[2]); });
// this works. I click on button 0 and get myInfo[0],
// on 1 and get myInfo[1], on 2 and get myInfo[2]
But replacing it with a loop does not work correctly. Instead, I always get the last element: myIfno[2] for any button I press.
var btns = $('.gotobtn');
var i = 0;
for (i = 0; i<3; i++){
var btnid = "#" + btns.get(i).id;
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]); });
}
// this does set the buttons on-click but when I click on them,
// all get the latest iteration, in this example myInfo[2]
Why is this? And how do I fix that, without defining each button manually?
I want to see how to do it in jquery.
Because: JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script
Replace:
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]);
});
With:
$(btnid).click(customFunction(i));
And declare this function outside the loop:
function customFunction(i) {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]);
}
your
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]); });
Must be outside your for loop.
#ibrahim mahrir is correct. It's caused by the same phenomena as described in 46039325 although this question is specific for JQuery binding and will probably be useful to some. (I saw the unanswered question in several places on the web)
It happens because I'm binding to i, and in the meantime i has changed to the last iteration (which is 2 in this example). I need to bind to the value of i while iterating.
This will happen (due to quirks in javascript) if I define the binding to a parameter of a function. The parameter is "dynamically created" each time and the value of that param (during that iteration) will be bound.
So when I finally do click on the second button (id:1, the first is id:0), it will invoke the method with the value of 1, correctly.
Here's an example of how the fix looks in jQuery:
$(function(){ // document ready
function btnaction(i){
var btns = $('.gotobtn');
$('#'+btns.get(i).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', gotoInfo[i]);
});
}
and I call it in the loop
for (i = 0; i<6; i++)
btnaction(i);
Alls well that ends well...

Setting passed in values to event listeners in a loop doesn't work

This feels like a very basic question, but I can't seem to find an answer t it. I have an array of clickable svg rects that have id's "texture-1", "texture-2" etc. The textureInput array holds the getElementById's for those rects, that call a changeTexture() function on click. I want "1" to be passed into the changeTexture() function when "texture-1" is clicked on etc.
Manually assigning these values works: e.g.
textureInput[0].addEventListener('click', function(){changeTexture("0")}, false);
textureInput[1].addEventListener('click', function(){changeTexture("1")}, false);
But doing the same thing in a loop does not:
for (var i=0; i<maxTextures; i++){
textureInput[i].addEventListener('click', function(){changeTexture(i)}
};
Is it passing in the then current value of "i" at event time - which is undefined outside the loop?
You need to create a closure so that a new copy of the i variable is made, otherwise it will use the one that belongs to the for loop. Try this:
for (var i=0; i<maxTextures; i++){
textureInput[i].addEventListener('click', (function(i) {
return function () {
changeTexture(i);
};
})(i));
};
Use a closure. One simple way is wrap an IIFE around the code within the loop
for (var i=0; i<maxTextures; i++){
(function(i){
textureInput[i].addEventListener('click', function(){changeTexture(i);}
})(i);
};

Raphael.js - registering multiple events to element

my problem is that I need handle multiple events for rectangle. That sound simple,
for example this works
node.click(function(e){
click(); // this is function defined in same scope, it works ok
});
node.mouseout(function(e){
mouseout();
});
But, I want to automatize this, so it should looks like this:
var events = new Array("click", "mouseout");
for(var i in events){
node[events[i]](function(e){
events[i](); /*THIS is problem, no matter if it is click or mouseout
this always fires function with same name as last item
in events array (in this case mouseout)
*/
}
}
Do you have any idea why a how I should solve it?
Your handlers created in a loop are sharing a variable. By the time they are called, the variable is the last value in the loop.
You have to use a technique I call "freezing your closures" so that each handler gets a separate copy of the shared variable. In your case, the shared variable that changes is i
Your other problem is that you want to call your functions "click/mouseout" from a string, so you have to get a handle to the function, right now your code is attempting to call "hello"() which does not work
Your last problems (but not a bug yet) are that you shouldn't use the Array constructor and you shouldn't use a for in loop to iterate over arrays.
function createHandler(eventName) {
return function(e) {
window[eventName]();
}
}
var events = ["click", "mouseout"];
for(var i=0; i < events.length; i++){
node[events[i]](createHandler(events[i]));
}
The above example is easier to comprehend but you could use self invoking anonymous functions to do the same thing
var events = ["click", "mouseout"];
for(var i=0; i < events.length; i++){
node[events[i]]((function(eventName){
return function(e) {
window[eventName]();
};
})(events[i]));
}

Javascript Array addEventListener

Interactive map with buttons in the shape of states, each button has the state abbreviation as an id, when a button/state is clicked I would like to fire the function "stateSelect" and send the state abbreviation with it so I know what's been pressed. Why doesn't the following work?
var stateList = new Array("AK","AL","AR","AS","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID",
"IL","IN","KS","KY","LA","MA","MD","ME","MH","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
"OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY");
for (var i = 0; i < stateList.length; i++) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
I obviously want to avoid 50 some lines of code but I'm not sure why this simple loop isn't working.
Because when the handler runs, it looks up the value of i, which is wherever it was after the loop finished.
You need to scope the i variable in a function:
function listenerForI( i ) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
for (var i = 0; i < stateList.length; i++) {
listenerForI( i );
}
Now the i referenced by the handler will be the parameter to the listenerForI function that was invoked. As such, that i will reference the value that was passed in from the for loop.
You have a scoping issue. Javascript is not block-scoped; it is function-scoped. Basically, you must create a new function whenever you wish to create a new variable in a loop.
The most elegant way to do so is as follows:
stateList.map(function(abbrev){
$(abbrev).mousedown(function(){stateSelect(abbrev)});
});
If you are not using jQuery, merely replace $(abbrev).mousedown with document.getElementById(abbrev).addEventListener.
(Just to preempt the people who go "map isn't standard"; it is in the javascript ECMA-262 standard 5th edition which has support from all browser vendors. If one is paranoid about supporting older browsers, one can just $.map.)
Here is how one would do so using a for loop; it's a bit uglier but it demonstrates the necessity of creating new closures via functions:
for(var i=0; i<stateList.length; i++)
(function(i){
$(stateList[i]).mousedown(...);
})(i);
Like I said, a bit uglier than necessary; you could also do this which is slightly less ugly, but is basically the same thing:
function createListener(abbrev) {
$(abbrev).mousedown(...);
}
for(var i=0; i<stateList.length; i++)
createListener(stateList[i]);

Dynamically Change HTML DOM event

I am trying to dynamically change an element's onClick event and I have something like the following:
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
Everything seems to work fine apart from the fact that the argument passed to 'existingFunction()' is the final value of i=4 each time it is called. Is there a way to bind a function to onclick that uses the value of i at the time of binding as opposed to what it seems to be doing at the moment and referencing the original i in the for-loop.
Also is is there a way of performing the same bind without having to create anonymous functions each time? so that I can directly reference 'existingFunction' in each onclick for performance reasons?
Cheers guys,
Yong
Change
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
to
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = createOneHandler(i);
}
function createOneHandler(number){
return function() {
existingFunction(number);
}
}
and it should work fine.
Working Demo
A good explanation is given here
JavaScript, time to grok closures
for the i being always 4, you have a scoping problem, I advise to read this. Scoping is are really important concept, so you have better to make sure to understand what's is going on.
a better code would be
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = existingFunction;
}
the onclick would pass an event has argument so you can know what element have been clicked
i.e.
function existingFunction(event){
// DO something here
}
you can read more about events there. IE does have the exact same event model as other browser so you would have to handle it.
Last bit, I advise you to use a JS framework(Jquery,ExtJS,DOJO,Prototype...) because it would simplify your task
the code you posted should work the way you intended, your problem with i=4 is elsewhere. edit: this is wrong, rageZ is right about the scoping problem.
re the other question: all you can do is offload the verbosity with
var f = function (i) { return function () { existingFunction(i); return false; } }
for (...) { document.getElementById(...).onclick = f(i); }
BTW, you should use something like jQuery for DOM manipulation (concise syntax), and perhaps Zeta (http://codex.sigpipe.cz/zeta/) for the function composition
var f = compose(false_, existingFunction);
for (...) { $(...).click(f(i));
Hooray! It's loop closures again! See 422784, 643542, 1552941 et al for some more discussion.
is there a way of performing the same bind without having to create anonymous functions each time?
Yes, in ECMAScript Fifth Edition you get function.bind:
for (var i = 1; i < 5; i++)
document.getElementById('element'+i).onclick= existingFunction.bind(window, i);
In the meantime since browsers don't yet generally support it you can monkey-patch an alternative implementation of bind (see the bottom of this comment for one such) built out of anonymous functions as a fallback.
Alternatively, assign the same event handler function to every element and just have it look at this.id to see which element number it is.

Categories

Resources