JS onclick not firing correctly - javascript

I have the following function. The problem is that instead of waiting for the user to click the image as expected, the function immediately fires the imgReplace function for each element in the images array.
Have I done something wrong?
Could the fact I'm using a separate Javascript routine based on Jquery be relevant here?
function setup () {
var images = document.getElementById("mycarousel");
images = images.getElementsByTagName("img");
for (var i = 0; i< images.length; i++) {
images[i].onclick = imgReplace (images[i]);
}
}

Wow I just fixed this embarrassing bug in some of my own code. Everybody else has gotten it wrong:
images[i].onclick = function() {imgReplace(images[i]);};
won't work. Instead, it should be:
images[i].onclick = (function(i) { return function() { imgReplace(images[i]); }; })(i);
Paul Alexander's answer is on the right track, but you can't fix the problem by introducing another local variable like that. JavaScript blocks (like the {} block in the "for" loop) don't create new scopes, which is a significant (and non-obvious) difference from Java or C++. Only functions create scope (setting aside some new ES5 features), so that's why another function is introduced above. The "i" variable from the loop is passed in as a parameter to an anonymous function. That function returns the actual event handler function, but now the "i" it references will be the distinct parameter of the outer function's scope. Each loop iteration will therefore create a new scope devoted to that single value of "i".

Your assigning the result of the call to imageReplace to the onclick handler. Instead wrap the call to imageReplace in it's own function
images[i].click = function(){ imgReplace( images[i] ) }
However, doing so will always replace the last image. You need to create a new variable to enclose the index
for (var i = 0; i< images.length; i++) {
var imageIndex = i;
images[i].onclick = function(){ imgReplace (images[imageIndex]); }
}

What you want to do here is:
images[i].onclick = function() {imgReplace(images[i]);}
try that.
Cheers

Related

JavaScript Function inside the loop

Can someone explain to me why JSLint complains about "Function inside the loop" with this example:
for (var i = 0; i < buttons.length; i++) {
(function(i) {
buttons[i].onclick = function(e) {
t.progressBars[t.current].update(buttons[i].getAttribute("data-value"));
}
})(i);
}
But dosen't when I change it to:
function makeHandler(i)
{
return function() {
t.progressBars[t.current].update(buttons[i].getAttribute("data-value"));
}
}
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = makeHandler(i);
}
I don't quite understand as it seems that with each loop iteration new function object has to be returned, even though it happens inside of makeHandler() function. Why the second example is ok with JS linters?
Quoting from linterrors,
var elems = document.getElementsByClassName("myClass"), i;
for (i = 0; i < elems.length; i++) {
(function (iCopy) {
"use strict";
elems[i].addEventListener("click", function () {
this.innerHTML = iCopy;
});
}(i));
}
What we have now captures the value of i at each iteration of the loop. This happens because JavaScript passes arguments to functions by value. This means that iCopy within the capturing function is not related to i in any way (except for the fact that they happen to have the same value at that point in time). If i changes later (which it does - on the next iteration of the loop) then iCopy is not affected.
This will work as we expect it to but the problem now is that the JavaScript interpreter will create an instance of the capturing function per loop iteration. It has to do this because it doesn't know if the function object will be modified elsewhere. Since functions are standard JavaScript objects, they can have properties like any other object, which could be changed in the loop. Thus by creating the function in the loop context, you cause the interpreter to create multiple function instances, which can cause unexpected behavior and performance problems. To fix the issue, we need to move the function out of the loop:
I would have liked to use Array.prototype.forEach here, like this
buttons.forEach(function(curButton) {
curButton.onclick = function(e) {
t.progressBars[t.current].update(curButton.getAttribute("data-value"));
};
});
Your two examples are not equivalent.
In the first, you are creating an anonymous function and calling it on every loop.
The inner function (the click event handler) is fine - you're assigning a new function - but it's the anonymous outer function that is inefficient in this context. In your second example the outer function is refactored out of the loop where is it only created once, instead of buttons.length times.

Why does my anonymous function not have the context of the current object in this example?

When I run this sample code in Google Chrome, the intended behavior--loading an image within a placeholder image tag on the current page--does not occur. I checked the value of currPic when showPic() is called, and it is "undefined." I know if I change the parameter to showPic from 'anchors[i]' to 'this', then it will work, but was trying to understand why this is so.
function showPic(currPic) {
var srcLoc = currPic.getAttribute("href");
var placeHolder = document.getElementById("placeholder");
placeHolder.setAttribute("src", srcLoc);
var imgLabel = document.getElementById("imglabel");
var currLinkTitle = currPic.getAttribute("title");
imgLabel.firstChild.nodeValue = currLinkTitle;
}
function prepareGallery() {
if(!(document.getElementsByTagName && document.getElementById)) return false;
var imgGallery = document.getElementById("imagegallery");
if(imgGallery) {
var anchors = imgGallery.getElementsByTagName("a");
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
}
}
Inside the anonymous function, anchors[i] provides a runtime reference. At the time the click occurs, anchors[i] no longer exists. While it existed at the time the assignment was made, it falls out of scope at the time of the click (since it's just an array reference). However, using this provides a solid reference to the immediate object that is always available at the time of the click.
More succinctly, anchors[i] is a reference to a position in an array (which leaves scope once the for loop exits). this is a reference to the dom element itself.
Because this would also work: showPic(document.getElementById(anchors[i].id)); - do you "get" it now (pun very much intended)?
Didn't see the obvious statement regarding how closures work, so here's my take on it.
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
Notice how you reference the i variable inside the loop? By the end of your loop, the value of i equals anchors.length.
So, when any of your onclick function is executed, that reference to i now points one position past the last index of anchors; this is why you see currPic is undefined.
One solution to this problem has been given in other answers: use this to reference the current anchor and don't pass anchors[i] to the onclick function.
As you may encounter similar situations, I'll show you another solution by closing over the value of i like so:
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = (function(i) {
// inside this function, i is closed over and won't change anymore
return function() {
showPic(anchors[i]);
return false;
}
}(i));
}

Javascript scope and calling a function

My code:
for (var i = 0; i < mapInfos.length; i++) {
var x = function () { doStuff(i); };
google.maps.event.addListenerOnce(mapInfos[i].map, 'tilesloaded', x);
}
The doStuff method simply alerts the value of i. mapInfos has two entries, so you'd expect it to alert 0 and 1, but instead it alerts 2 and 2. I can appreciate vaguely why it is doing this (although var i should keep it local to the scope of the loop?) but how can I make it work as intended?
edit — note that when first posted, the original question included a link to a jsfiddle that seemed to be a relevant example of what the current question is trying to achieve, only it appears to work ...
The code in the jsfiddle works because there's only one "i" in that code. The "i" used in the second loop (where the functions are actually called) is the same "i" as used in the first loop. Thus, you get the right answer because that second loop is running "i" through all the values from zero through four again. If you added:
i = 100;
functions[0]();
you'd get 100 printed out.
The only way to introduce a new scope in JavaScript is a function. One approach is to write a separate "function maker" function:
function makeCallback(param) {
return function() {
doStuff(param);
};
}
Then in your loop:
for (var i = 0; i < mapInfos.length; i++) {
var x = makeCallback(i);
google.maps.event.addListenerOnce(mapInfos[i].map, 'titlesloaded', x);
}
That'll work because the call to the "makeCallback" function isolates a copy of the value of "i" into a new, unique instance of "param" in the closure returned.
Create a new scope for it.
Functions create scope.
function doStuffFactory(i) {
return function () { doStuff(i); };
}
for (var i = 0; i < mapInfos.length; i++) {
var x = doStuffFactory(i);
google.maps.event.addListenerOnce(mapInfos[i].map, 'tilesloaded', x);
}
Change it to
var x = function (param) { doStuff(param); };
Obviously what is going on is that you are alerting a variable that is changing. With the above change it copies it so even if i changes it will still alert the right value.
Javascript doesn't have block scope, so you don't get an x that's local to the loop. Yea!
It has function scope, though.
Yep, weird isn't it!Pointy has an explanation
I have no idea why your first example worked (I wasn't expecting it to) Pointy has an explanation of why your first example worked - The reason why your second one doesn't is because i is scoped to the function containing the for loop, not to the scope defined by the for loop. In fact the only things that have scope in JavaScript are functions. This means that by the time your function gets executed i is 2.
What you need to do is create a scope, for example:
for (var i = 0; i < mapInfos.length; i++) {
var x = (function() {
return function () { doStuff(i); };
})(i);
google.maps.event.addListenerOnce(mapInfos[i].map, 'tilesloaded', x);
}
See JavaScript closures in for-loops for more.

Javascript Closure Problem

I know this kind of question gets asked alot, but I still haven't been able to find a way to make this work correctly.
The code:
function doStuff () {
for (var i = 0; i< elementsList.length; i++) {
elementsList[i].previousSibling.lastChild.addEventListener("click", function(){
toggle(elementsList[i])}, false);
}
} // ends function
function toggle (element) {
alert (element);
}
The problem is in passing variables to the toggle function. It works with the this keyword (but that sends a reference to the clicked item, which in this case is useless), but not with elementsList[i] which alerts as undefined in Firefox.
As I understood it, using anonymous functions to call a function is enough to deal with closure problems, so what have I missed?
Try:
function startOfFunction() {
for (var i = 0; i< elementsList.length; i++) {
elementsList[i].previousSibling.lastChild.addEventListener(
"click",
(function(el){return function(){toggle(el);};})(elementsList[i]),
false
);
}
} // ends function
function toggle (element) {
alert (element);
}
The Problem is, that you want to use the var i! i is available in the onClick Event, (since closure and stuff). Since you have a loop, i is counted up. Now, if you click on any of the elements, i will always be elementsList.length (since all event functions access the same i )!
using the solution of Matt will work.
As an explanation: the anonymous function you use in the for loop references the variable "i" to get the element to toggle. As anonymous functions use the "live" value of the variable, when somebody clicks the element, "i" will always be elementsList.length+1.
The code example from Matt solves this by sticking the i into another function in which it is "fixated". This always holds true:
If you iterate over elements attaching events, do not use simple anonymous functions as they screw up, but rather create a new function for each element. The more readable version of Matts answer would be:
function iterate () {
for (var i = 0; i < list.length; i++) {
// In here, i changes, so list[i] changes all the time, too. Pass it on!
list[i].addEventListener(createEventFunction(list[i]);
}
}
function createEventFunction (item) {
// In here, item is fixed as it is passed as a function parameter.
return function (event) {
alert(item);
};
}
Try:
function doStuff () {
for (var i = 0; i< elementsList.length; i++) {
(function(x) {
elementsList[x].previousSibling.lastChild.addEventListener("click", function(){
toggle(elementsList[x])}, false);
})(i);
}
} // ends function
I think it might be an issue with passing elementsList[i] around, so the above code has a closure which should help.

jQuery, hover method and closure

Have been struggling with Javascript closure for a while trying to wrap brain around function scopes, but I think they're wrapping around me instead. I've looked at a number of posts (Nyman's was the most helpful) but obviously still don't get it. Trying to run a loop over the hover method in jQuery. Need hover functions to ultimate trigger more than one action each, but would be happy to get them working with a single image swap each for now.
$(document).ready(function() {
imageSource = [];
imageSource[0] = 'images/img0.png' //load 0 position with "empty" png
imgArea = [];
for (var i=1; i<11; i++) {
(function( ){ //anonymous function for scope
imageSource[i] = 'images/img' + i + '.png';
imgArea[i] = '#areamap_Img' + i;
// running console.log here gives expected values for both
$(imgArea[i]).hover( //imgArea[i] (selector) works correctly here
function() {
$('#imgSwap').attr('src',imageSource[i]); // imageSource[i] is undefined here
},
function() {
$('#imgSwap').attr('src','images/img0.png');
});
})(); // end anonymous function and execute
}; // for loop
});
Tried the idea of using an anonymous function for scoping from another jQuery post. Seems to work OK but throws an undefined for the array value in the first hover function, I guess because it's an inside function (hardcoded image sources work correctly there).
There is indeed a problem with your closures, and it has to do with your usage of the var i. Since your anonymous function has no local version of i, it's using the version of the function above it. However, when it tries to access i at a later date, i == 11 (since that's what made the loop terminate). To fix this, you need to declare a local version of i in each anonymous function, like this:
for (var i=1; i<11; i++) {
(function( ){ //anonymous function for scope
var index = i; // The important part!
// It's not technically necessary to use 'index' here, but for good measure...
imageSource[index] = 'images/img' + index + '.png';
imgArea[index] = '#areamap_Img' + index;
$(imgArea[index]).hover(
function() {
$('#imgSwap').attr('src',imageSource[index]); // Here's where `index` is necesssary.
},
function() {
$('#imgSwap').attr('src','images/img0.png');
});
})(); // end anonymous function and execute
}; // for loop
Additionally, there's a small problem in your code you should fix just for good measure. You're not accessing your local variables correctly; you should use:
var imageSource = [];
var imageSource[0] = 'images/img0.png' //load 0 position with "empty" png
var imgArea = []
Without the "var", you're declaring and accessing global variables. (If this is your intended behavior then I apologize.)

Categories