This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 7 years ago.
I'm trying to do the following:
I have a set of images and select (dropdown) HTML elements, 30 of each one. I'm trying to use AddEventListener on a loop from 1 to 30 so that when I change the value of the select, the image src is updated (and the image changes).
The AddEventListener function is this one:
function AddEventListener(element, eventType, handler, capture)
{
if (element.addEventListener)
element.addEventListener(eventType, handler, capture);
else if (element.attachEvent)
element.attachEvent("on" + eventType, handler);
}
I tried this and it worked:
var urlfolderanimalimages = "http://localhost/animalimages/";
var testselect = "sel15";
var testimg = "i15";
AddEventListener(document.getElementById(testselect), "change", function(e) {
document.getElementById(testimg).src = urlfolderanimalimages + document.getElementById(testselect).value;
document.getElementById(testimg).style.display = 'inline';
if (e.preventDefault) e.preventDefault();
else e.returnResult = false;
if (e.stopPropagation) e.stopPropagation();
else e.cancelBubble = true;
}, false);
But then I tried to call it in a loop and it doesn't work. The event is added, but when I change any select, it will update the last one (the image with id i30).
var urlfolderanimalimages = "http://localhost/animalimages/";
for (k=1;k<=30;k++) {
var idselect = "sel" + k;
var idimage = "i" + k;
AddEventListener(document.getElementById(idselect), "change", function(e) {
document.getElementById(idimage).src = urlfolderanimalimages + document.getElementById(idselect).value;
document.getElementById(idimage).style.display = 'inline';
if (e.preventDefault) e.preventDefault();
else e.returnResult = false;
if (e.stopPropagation) e.stopPropagation();
else e.cancelBubble = true;
}, false);
}
What am I doing wrong? I'm new to JavaScript (and programming in general), so sorry for the vomit-inducing code :(
The problem is that you are 'closing' over the same variable.
var does not declare a variable1. It simply annotates a 'stop look-up' for an identifier within a given execution context.
Please see Javascript Closures "FAQ Notes" for all the nice details. The sections on 'execution context' and 'scope chain' are most interesting.
The common idiom is perform a double-binding to create a new execution context.
E.g.
var k
for (k = 1; k < 10; k++) {
setTimeout((function (_k) {
return function () {
alert(_k)
}
})(k), k * 100)
}
As of JavaScript 5th Edition there is Function.bind (it can also be implemented in 3rd edition such as bind from prototype.js), which can be used as such:
var k
for (k = 1; k < 10; k++) {
setTimeout((function () {
alert(this)
}).bind(k), k * 100)
}
1 This construct is referred to as a Variable Declaration in the ECMAScript specification. However, this statements, as misleading as it may be viewed, is stressed to bring home a point: also see "variable hoisting".
I also found this solution at http://meshfields.de/event-listeners-for-loop/:
var myArr = [0,1,2,3];
for (var i = 0; i < myArr.length; i+=1) {
(function (i) {
document.getElementById('myDOMelement' myArr[i]).onclick = function () {
if (window.console.firebug !== undefined) {
console.log('myDOMelement' myArr[i]);
}
else {
alert('myDOMelement' myArr[i]);
}
};
}) (i);
}
Related
The handler inside of my for loop might be out of scope and only prints "Last Event added" in console but doesn't loop through each element in the array. No sure where I'm going wrong here, but I need help attaching the event listener to each.
(function () {
if (document.addEventListener) {
this.addEvent = function (elem, type, fn) {
elem.addEventListener(type, fn, false);
};
} else if (document.attachEvent) {
this.addEvent = function (elem, type, fn) {
var bound = function () {
return fn.apply(elem, arguments);
};
elem.attachEvent("on" + type, bound);
return bound;
};
}
if (document.getElementsByClassName) {
this.getClass = function (className) {
return document.getElementsByClassName(className);
};
} else if (document.querySelectorAll) {
this.getClass = function (className) {
return document.querySelectorAll("." + className);
};
}
var elem = getClass("images"),
display = getClass("display_box"),
rolloverImage = function (e) {
console.log("Event 'rolloverImage' triggered");
};
console.log(display);
console.log(elem);
console.log(elem.length);
for (var i = 0; i < elem.length; i++) {
document.addEvent(elem[i], "mouseover", rolloverImage);
if (i = elem.length) {
console.log("Last event added");
} else {
console.log("Event added to " + elem[i]);
}
};
})();
A fiddle is available here: http://jsfiddle.net/bNL5C/
if you are wanting your addEvent function to be on the document object use document.addEvent = not this.addEvent = as that is putting it on the global object window
Also it doesnt loop through all of them because you are assigning i to the elem array length instead of comparing.
if (i = elem.length) {
should be
if (i == elem.length) {
Because of this on the first iteration through the loop causes i to be the value of elem.length and since i is now not < elem.length your loop exits.
One problem is that in your for loop, the if (i = elem.length) will always be true, as you are using a single assignment =, and changing the value of i. This needs to be changed to if (i == elem.length) or perhaps you'd prefer to use ===.
I'm trying to develop a javascript object that creates a menu in html.
The function receives an object as an argument. Among the object elements is a function that should be executed in an event handler called from a method of my object.
Here is my code :
Menu = function(config) {
var j = 0;
this.config = config;
this.make = function() {
for (i = 0; i < this.config.items.length; i++) {
var vid = document.createElement("div");
vid.className = this.config.cls;
vid.id += i;
document.body.appendChild(vid);
var txt = document.createTextNode(this.config.items[i]);
var pp = document.createElement("p");
pp.appendChild(txt);
vid.appendChild(pp);
}
document.addEventListener("keydown", this.scrolldown, false);
document.onkeydown = function(e) {
var keyCode = e.keyCode;
alert("functional");
if (keyCode == 40) {
alert("You hit key down");
var et = document.getElementById(j);
this.config.trait1(et);
j = j + 1;
} else {
alert("no");
}
}
};
return this;
};
when I call the function make after instantiating the object I have my elements created but my event isn't handled because of :
Uncaught TypeError: Cannot call method 'trait1' of undefined .
Can anyone help me? I saw many answers of the same question but none of the suggested solutions worked.
this inside the Menu function is not the same as this inside the onkeydown function.
Store the value of this in another variable and use that.
Menu = function () {
var myMenu = this; // I'm assuming that you will be calling `new Menu()`
document.onkeydown = function () {
myMenu.config.etc.etc.etc
}
}
I am using Typeahead by twitter. I am running into this warning from Intellij. This is causing the "window.location.href" for each link to be the last item in my list of items.
How can I fix my code?
Below is my code:
AutoSuggest.prototype.config = function () {
var me = this;
var comp, options;
var gotoUrl = "/{0}/{1}";
var imgurl = '<img src="/icon/{0}.gif"/>';
var target;
for (var i = 0; i < me.targets.length; i++) {
target = me.targets[i];
if ($("#" + target.inputId).length != 0) {
options = {
source: function (query, process) { // where to get the data
process(me.results);
},
// set max results to display
items: 10,
matcher: function (item) { // how to make sure the result select is correct/matching
// we check the query against the ticker then the company name
comp = me.map[item];
var symbol = comp.s.toLowerCase();
return (this.query.trim().toLowerCase() == symbol.substring(0, 1) ||
comp.c.toLowerCase().indexOf(this.query.trim().toLowerCase()) != -1);
},
highlighter: function (item) { // how to show the data
comp = me.map[item];
if (typeof comp === 'undefined') {
return "<span>No Match Found.</span>";
}
if (comp.t == 0) {
imgurl = comp.v;
} else if (comp.t == -1) {
imgurl = me.format(imgurl, "empty");
} else {
imgurl = me.format(imgurl, comp.t);
}
return "\n<span id='compVenue'>" + imgurl + "</span>" +
"\n<span id='compSymbol'><b>" + comp.s + "</b></span>" +
"\n<span id='compName'>" + comp.c + "</span>";
},
sorter: function (items) { // sort our results
if (items.length == 0) {
items.push(Object());
}
return items;
},
// the problem starts here when i start using target inside the functions
updater: function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
};
$("#" + target.inputId).typeahead(options);
// lastly, set up the functions for the buttons
$("#" + target.buttonId).click(function () {
window.location.href = me.format(gotoUrl, $("#" + target.inputId).val(), target.destination);
});
}
}
};
With #cdhowie's help, some more code:
i will update the updater and also the href for the click()
updater: (function (inner_target) { // what to do when item is selected
return function (item) {
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}}(target))};
I liked the paragraph Closures Inside Loops from Javascript Garden
It explains three ways of doing it.
The wrong way of using a closure inside a loop
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Solution 1 with anonymous wrapper
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Solution 2 - returning a function from a closure
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
Solution 3, my favorite, where I think I finally understood bind - yaay! bind FTW!
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
I highly recommend Javascript garden - it showed me this and many more Javascript quirks (and made me like JS even more).
p.s. if your brain didn't melt you haven't had enough Javascript that day.
You need to nest two functions here, creating a new closure that captures the value of the variable (instead of the variable itself) at the moment the closure is created. You can do this using arguments to an immediately-invoked outer function. Replace this expression:
function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
With this:
(function (inner_target) {
return function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}
}(target))
Note that we pass target into the outer function, which becomes the argument inner_target, effectively capturing the value of target at the moment the outer function is called. The outer function returns an inner function, which uses inner_target instead of target, and inner_target will not change.
(Note that you can rename inner_target to target and you will be okay -- the closest target will be used, which would be the function parameter. However, having two variables with the same name in such a tight scope could be very confusing and so I have named them differently in my example so that you can see what's going on.)
In ecmascript 6 we have new opportunities.
The let statement declares a block scope local variable, optionally initializing it to a value.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
Since the only scoping that JavaScript has is function scope, you can simply move the closure to an external function, outside of the scope you're in.
Just to clarify on #BogdanRuzhitskiy answer (as I couldn't figure out how to add the code in a comment), the idea with using let is to create a local variable inside the for block:
for(var i = 0; i < 10; i++) {
let captureI = i;
setTimeout(function() {
console.log(captureI);
}, 1000);
}
This will work in pretty much any modern browser except IE11.
I'm programming in Javascript.
I need to dynamically generate the event handlers for the onclick event.
Here above the code:
for (var i = 1; i < CurrValue; i++) {
getId('btnReadRFIDTag_' + i).onclick = function ()
{
ReadRFIDTag(getId('txtCode_' + i));
};
}
The problem, obviously is that at run time the i variable get the latest value and not the right value.
But I can't fix the problem, because the handler does not allow parameters for the function.
How can I solve this problem?
Thank you.
Have a good day.
Three options:
Use a Builder Function
The usual way is to have a builder function for the handlers:
for (var i = 1; i < CurrValue; i++) {
getId('btnReadRFIDTag_' + i).onclick = buildHandler(i);
}
function buildHandler(index) {
return function() {
ReadRFIDTag(getId('txtCode_' + index));
};
}
The function built by the buildHandler closes over the index argument from that call to buildHandler, rather than over i, and so it sees the right value.
Use ES5's Function#bind
If you can rely on ES5 (or you include an ES5 shim, as this is a shimmable feature), you can do it with Function#bind:
// Using Function#bind (option 1)
for (var i = 1; i < CurrValue; i++) {
var elm = getId('btnReadRFIDTag_' + i);
elm.onclick = function(index) {
ReadRFIDTag(getId('txtCode_' + index));
}.bind(elm, i);
}
Used like that, it creates unnecessary extra functions, so you could also use it like this:
// Using Function#bind (option 2)
for (var i = 1; i < CurrValue; i++) {
var elm = getId('btnReadRFIDTag_' + i);
elm.onclick = myHandler.bind(elm, i);
}
function myHandler(index) {
ReadRFIDTag(getId('txtCode_' + index));
}
Use Event Delegation
Instead of putting a handler on each button, if they're all in a container (and ultimately, of course, they are, even if it's just document.body), you can put the handler on that container and then use event.target to find out which button was clicked. I wouldn't do that with old-style onclick, but you can with addEventListener or attachEvent:
var container = /* ...get the container... */;
if (container.addEventListener) {
container.addEventListener('click', clickHandler, false);
}
else if (container.attachEvent) {
container.attachEvent('onclick', function(e) {
return clickHandler.call(this, e || window.event);
});
}
else {
// I wouldn't bother supporting something that doesn't have either
}
function clickHandler(e) {
var btn = e.target;
while (btn && btn !== this && btn.id.substring(0, 15) !== "btnReadRFIDTag_") {
btn = btn.parentNode;
}
if (btn && btn !== this) {
ReadRFIDTag(getId('txtCode_' + btn.id.substring(15)));
}
}
The way that works is by hooking the click event on the container, and then when the click bubbles up (down?) to it, it looks at where the click originated and sees if that (or an ancestor of it) is one of the buttons we care about. If it is, we act on it.
Here is the link to the page with the script.
http://signsourceak.com/index1.html
Here is my script and for some reasons all the functions fire with out mouse over. Can anyone tell me what is wrong with my script
window.onload = sliding;
var tags = new Array('tag1','tag2','tag3','tag4','tag5','tag6','tag7','tag8');// List of headings
var pics = new Array('popout1','popout2','popout3','popout4','popout5','popout6','popout7','popout8');// list of images that slide out
function sliding(){ // assing event
for(var i=0; i< tags.length; i++){
document.getElementById(tags[i]).onmouseover = slideout(tags[i],pics[i]); // <-- The Problem is Here Function runs with out the actual event
document.getElementById(tags[i]).onmouseout = slidein(tags[i],pics[i]);
//alert('this worked,'+ tags[i] + pics[i]);
}
}
function slideout(hid,picid){
document.images[picid].style.visibility = "visible";
document.images[picid].style.MozOpacity = 0.7;// need browser compatability
moveout(hid,picid);
}
function moveout(hid,picid){
if(currpos(picid) > 0){
document.images[picid].style.top = currpos(picid) - 1 + "px";
setTimeout(moveout,10);
}else{
clearTimeout(moveout);
}
function currpos(element){
return document.getElementById(element).offsetTop;
}
}
function slidein(hid,picid){
document.images[picid].style.MozOpacity = 0.5;// need browser compatability
movein(hid,picid);
}
function movein(hid,picid){
if(currpos(picid) < 210){
document.images[picid].style.top = currpos(picid) + 1 + "px";
setTimeout(movein,10);
}else{
clearTimeout(movein);
document.images[picid].style.visibility = "hidden";
}
function currpos(element){
return document.getElementById(element).offsetTop;
}
}
that is not how to use clearTimeOut.
setTimeout returns a timer id that have to be passed to clearTimeOut:
var timer = setTimeout( fn, 10 );
clearTimeout( timer);
You are assigning the result of slideout() and slidein() as the handlers. You also need to isolate the closure variables; self calling functions will make sure the the i looping variable is not shared by all the event handlers
function sliding(){ // assing event
for(var i=0; i< tags.length; i++){
document.getElementById(tags[i]).onmouseover =(function(index){
return function() {
slideout(tags[index],pics[index]);
}
})(i);
document.getElementById(tags[i]).onmouseout = (function(index){
return function() {
slidein(tags[index],pics[index]);
}
})(i);
}
}
One problem is here:
function sliding(){ // assing event
for(var i=0; i< tags.length; i++){
document.getElementById(tags[i]).onmouseover = slideout(tags[i],pics[i]); // <-- The Problem is Here Function runs with out the actual event
document.getElementById(tags[i]).onmouseout = slidein(tags[i],pics[i]);
//alert('this worked,'+ tags[i] + pics[i]);
}
}
In that loop, you're calling the "slideout" and "slidein" functions, though it's obvious that that's not what you want to do. What you want is to assign a function that calls "slideout" or "slidein" the appropriate way. To do that, you'll need another layer of function:
function makeHandlers(index) {
return {
'out': function() { slideout(tags[index], pics[index]; },
'in': function() { slidein(tags[index], pics[index]; }
};
}
function sliding() {
for (var i = 0; i < tags.length; ++i) {
var handlers = makeHandlers(i), tag = document.getElementById(tags[i]);
tag.onmouseover = handlers.in;
tag.onmouseout = handlers.out;
}
}
As #BiAiB notes, your calls to "setTimeout" and "clearTimeout" need some attention too.