Pass variable to closure on event trigger - javascript

I have a loop where I define a closure for an event:
for (var i = 0; i < 3; i++) {
obj.onload = function(e) {
me.myFunction(e, i);
};
}
I need to pass that counter variable to the function that is called when the load event is triggered. I always get the same value being passed.
How can I pass the right counter value to the function?

The usual way to solve this is with a builder function, so that the handler closes over something that doesn't change:
function buildHandler(index) {
return function(e) {
me.myFunction(e, index);
};
}
for (var i = 0; i < 3; i++) {
obj.onload = buildHandler(i);
}
You can do that with an immediately-invoked function expression (IIFE), but in theory it creates and throws away a function on every loop (I expect most JavaScript engines these days optimize that) and I, at least, think it's a lot harder to read:
for (var i = 0; i < 3; i++) {
obj.onload = (function(index) {
return function(e) {
me.myFunction(e, index);
};
})(i);
}
There's an ES5 feature, Function#bind, that you can use if you want to use me.myFunction directly, but you'd have to change the order in which you were expecting the arguments (index first, then the event):
for (var i = 0; i < 3; i++) {
obj.onload = me.myFunction.bind(me, i);
}
bind creates a function that, when called, calls the original function with this set to the first argument (so, me above), then any further arguments you give it, and then any arguments that were used when calling the function. (That's why the order myFunction was expecting them had to change to index, e rather than e, i.)
On older browsers, you need a shim for Function#bind.

Related

Passing arguments to named function jshint warning

I have to use external/named functions for hover, like this:
function onMouseIn(e){
// Do stuff
}
function onMouseOut(e){
// Do stuff
}
for (var btn_index = 0; btn_index < 3; btn_index++) {
btn.hover(onMouseIn,onMouseOut)
}
However i also need to pass arguments to the functions, so i figured out i'd use unnamed functions for mouseIn and mouseOut events and call my named functions onMouseIn and onMouseOut repectively with the arguments i want. I also want to pass the event and the jquery object. So my code becomes something like this
function onMouseIn(e,btn){
// Do stuff using arg
}
function onMouseOut(e,btn){
// Do stuff using arg
}
for (var btn_index = 0; btn_index < 3; btn_index++) {
btn.hover(
function(e) {onMouseIn(e,$(this))},
function(e) {onMouseOut(e,$(this))})
}
But then jshint give me this warning
Don't make functions within a loop.
So are there any other ways to pass arguments to named functions? If not then how can i make jshint to hide the warning?
The warning is suggesting to try preparing the functions outside of the loop, if possible, so they can be reused throughout the loop:
// ...onMouseIn & onMouseOut...
function onHoverStart() {
onMouseIn("arg");
}
function onHoverStop() {
onMouseOut("arg");
}
for (var btn_index = 0; btn_index < 3; btn_index++) {
btn.hover(onHoverStart, onHoverStop);
}
When using function expressions within the loop (at least, when closures aren't needed), each iteration creates 2 new Function objects, with each pair a duplicate of the first. JSHint flags it as potentially wasteful.
Based on your comment, if one of the argument you need to provide is the iterator, btn_index, you can do that by modifying onHoverStart and onHoverStop to be called and used as closures for each event.
// ...onMouseIn & onMouseOut...
function onHoverStart(index) { // closure for index
return function (event) { // enclosed event handler
onMouseIn("arg", index, event); // revise the argument order here as needed
};
}
function onHoverStop(index) {
return function (event) {
onMouseOut("arg", index, event);
};
}
for (var btn_index = 0; btn_index < 3; btn_index++) {
btn.hover(onHoverStart(btn_index), onHoverStop(btn_index));
}
(For more details on closures, see "How do JavaScript closures work?" and "JavaScript closure inside loops – simple practical example")
You can use bind here
change the code to the following
function onMouseIn(arg){
// Do stuff using arg
}
function onMouseOut(arg){
// Do stuff using arg
}
for (var btn_index = 0; btn_index < 3; btn_index++) {
btn.hover(onMouseIn.bind(null,"arg"),onMouseOut.bind(null,"arg"));
}
Hope it helps

Correct usage of closure in for loop?

I'm adding an event listener to some elements I'm looping through and need a closure in order to preserve the index in the event function.
<button>solution 1</button>
<button>solution 2</button>
<script>
var buttons = document.getElementsByTagName('button');
for (var i = 0; i < 3; i++) {
var log = (function closure(number) {
return function () {
console.log(number);
};
})(i);
buttons[0].addEventListener("click", log);
}
for (var i = 0, len = 3; i < len; i++) {
(function (i) {
var log = function () {
console.log(i);
};
buttons[1].addEventListener("click", log);
})(i);
}
</script>
http://jsfiddle.net/paptd/11/
Both these solutions output 0, 1, 2 correctly (try 'wrong' to see what happens without a closure) but I'm trying to understand which one I should use and why.
Which way is the correct way of doing it?
The first one works because you are defining a closure, returning a function from it, then assigning that function to a listener.
The second one seems more proper, since the closure encompasses the entire loop content, making it more obvious that the value of i is to be "locked" there.
You shouldn't use any of these--you're creating n identical functions inside of your loop. You should refactor your code into a named function that returns the event handler:
var buttons = document.getElementsByTagName('button');
function createHandler(number) {
return function () {
console.log(number);
};
}
for (var i = 0; i < 3; i++) {
buttons[0].addEventListener("click", createHandler(i));
}
Example: http://jsfiddle.net/paptd/12/

Wrong Parameter is passed to function

I have the following code that adds an onmouseover event to a bullet onload
for (var i = 0; i <= 3; i++) {
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
}
This is the function that it is calling. It is supposed to add a css class to the menu item as the mouse goes over it.
function addBarOnHover(node) {
document.getElementById('menu').getElementsByTagName('li')[node].className = "current_page_item"; }
When the function is called, I keep getting the error:
"document.getElementById("menu").getElementsByTagName("li")[node] is
undefined"
The thing that is stumping me is I added an alert(node) statement to the addBarOnHover function to see what the value of the parameter was. The alert said the value of the parameter being passed was 4. I'm not sure how this could happen with the loop I have set up.
Any help would be much appreciated.
This is a common problem when you close over an iteration variable. Wrap the for body in an extra method to capture the value of the iteration variable:
for (var i = 0; i <= 3; i++) {
(function(i){ //here
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
})(i); //here
}
an anonymous function is created each time the loop is entered, and it is passed the current value of the iteration variable. i inside the anonymous function refers to the argument of this function, rather than the i in the outer scope.
You could also rename the inner variable for clarity:
for(var i=0; i<=3; i++){
(function(ii){
//use ii as i
})(i)
}
Without capturing the iteration variable, the value of i when it is finally used in the anonymous handler has been already changed to 4. There's only one i in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead.
i is being passed as a reference (not by value), so once the onmouseover callback is called, the value of i has already become 4.
You'll have to create your callback function using another function:
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('li');
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = (function(i) {
return function() {
addBarOnHover(i);
};
})(i);
}
You could make it a little more readable by making a helper function:
var createCallback = function(i) {
return function() {
addBarOnHover(i);
};
};
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = createCallback(i);
}

Deleting of an item, indexing problem...

for (var i = 0; i < max; i++) {
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(i, 1);
});
}
When i click on a button, a new table is added... i change some information in table and again add another table... and i delete this table... but the array value does delete the first index alone.
The Value of i is always zero and it deletes the first item in the Array. How can i ensure that for each table it has different ID.
For each click i feel so that i value always starts from Zero....
You're overwriting the value of i at each loop. You could use an anonymous function to pass the variable by value:
for (var i = 0; i < max; i++) {
(function(i){ //i is now defined within the scope of this anonymous function
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(i, 1);
}, true);
})(i); //Pass i to the anonymous function
}
Note that i can have a different name inside the anonymous function, without making a difference.
ANother notice: Pass three arguments to addEventListener, because many browsers will throw an error if the third argument is omitted.
Because i scope is outside event. You need get index from variable e, or maybe work this code:
for (var i = 0; i < max; i++) {
var ii = i;
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(ii, 1);
});
}
But I am not sure it works on javascript.

Scope and timing problems with for loops and setTimeout.

This should be easy but my head seems to be frazzled just now.
The result I'm aiming for is 0,1,2,0,1,2
My code so far.... here is a fiddle to play with.
function delayedLoad(page){
console.log(page);
};
function loadContent(){
var i,
len = 3
for (i = 0; i < len; i++) {
console.log(i);
setTimeout(function(){
delayedLoad(i);
},3000*i);
}
};
$(document).ready(function(){
// Load the content for the first time.
loadContent();
});
This is a very common issue people hit.
The trouble is that the function you're passing to setTimeout in each iteration is referencing the same i variable.
JavaScript does not have block scope, only function scope. So to create a new scope that will retain the i value you want, you need to invoke a function inside the loop, passing it i.
Example: http://jsfiddle.net/GNwhR/3/
function set_up_timeout( j ) {
setTimeout(function(){
delayedLoad( j );
},3000*j);
}
function loadContent(){
var i,
len = 3
for (i = 0; i < len; i++) {
console.log(i);
set_up_timeout( i );
}
};
This places your setTimeout call in another function which is invoked and passed i in each iteration. This creates a new variable scope where j in the function references the proper value.
Another approach is to have the invoked function return a function:
Example: http://jsfiddle.net/GNwhR/4/
function set_up_callback( j ) {
return function(){
delayedLoad( j );
};
}
function loadContent(){
var i,
len = 3
for (i = 0; i < len; i++) {
console.log(i);
setTimeout( set_up_callback( i ), 3000*i );
}
};
This one calls set_up_callback, which returns a function that references j.
Finally, another common approach is to use an IIFE (immediately invoked function expression) to eliminate the named function. This is less clear IMO:
Example: http://jsfiddle.net/GNwhR/5/
function loadContent(){
var i,
len = 3
for (i = 0; i < len; i++) {
console.log(i);
setTimeout((function( j ){
return function() {
delayedLoad( j );
};
})( i ),3000*i);
}
};
This is basically the same as the previous example in that it returns a function. The main difference is that the function that returns the function is created and invoked inside the loop itself.
All of these illustrate the same concept, that you need to scope a variable if you wish to asynchronously reference it later and have it retain its expected value.
patrick's explanation is pretty well right on. Here's a bit of code to help you get around it.
function delayedLoad(page){
console.log(page);
};
function loadContent(){
var i,
len = 3
for (i = 0; i < len; i++) {
console.log('in loop', i);
setTimeout($.proxy(function(){
delayedLoad(this.page);
}, {page: i}),3000*i);
}
};
$(document).ready(function(){
// Load the content for the first time.
loadContent();
});
jQuery's proxy just resets what 'this' is pointing to in a function.. so, you can replace passing in a number to just resetting the scope :) (if that makes any sense.. long day)

Categories