Scope and timing problems with for loops and setTimeout. - javascript

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)

Related

Pass variable to closure on event trigger

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.

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/

jquery callback anonymous function closure

I've declared the variable elem in both of the loops below; however, when the anonymous function in the first loop gets called (after the 400ms fadeOut effect is finished) the elem seems to refer to the value of elem which was assigned in the second loop. In other words, the code works correctly if you rename elem in the second loop to any other variable name.
Is there a way to make a closure around the anonymous function so that the value of elem is not changed in the context of the anonymous function?
for (var i = 0; i < outs.length; i++) {
var elem = this.elementAtPoint(outs[i]);
$(elem).fadeOut(400, function () {
$(elem).removeClass("white black queen"); //UPDATE
$(elem).show();
});
//$(elem).css("background", "red");
}
for (var i = 0; i < ins.length; i++) {
var elem = this.elementAtPoint(ins[i]);
var piece = this.board.pieceAt(ins[i]);
$(elem).hide();
$(elem).addClass(this.classForPiece(piece));
$(elem).fadeIn(400);
}
You could use a anonymous self executing function
for (var i = 0; i < outs.length; i++) {
(function(elem){
$(elem).fadeOut(400, function () {
$(elem).removeClass("white black queen"); //UPDATE
$(elem).show();
});
//$(elem).css("background", "red");
})(this.elementAtPoint(outs[i]));
}
in javascript, variable scope is decided by function, so you can wrap the two for loops in two function. a wrapper function example:
(function(){
//put you for loop here
})();
I would avoid creating lots of closures inside loops, they would be inefficient as they create copies of the current scope. Instead I'd simply create a couple of extra functions then call them inside your loops:
var fadeInPiece = function(elem, piece){
$(elem).hide();
$(elem).addClass(this.classForPiece(piece));
$(elem).fadeIn(400);
}
var fadeOutElem = function(elem){
$(elem).fadeOut(400, function () {
$(elem).removeClass("white black queen"); //UPDATE
$(elem).show();
});
}
for (var i = 0; i < outs.length; i++)
fadeOutElement(this.elementAtPoint(outs[i]));
for (var i = 0; i < ins.length; i++)
fadeInPiece(this.elementAtPoint(ins[i]), this.board.pieceAt(ins[i]));

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

Use queried json data in a function

I have a code similar to this:
$.ajax({
success: function(data) {
text = '';
for (var i = 0; i< data.length; i++) {
text = text + '' + data[i].Name + "<br />";
}
$("#SomeId").html(text);
for (var i = 0; i< data.length; i++) {
$("#Data_"+i).click(function() {
alert(data[i]);
RunFunction(data[i]);
return false;
});
}
}
});
This gets an array of some data in json format, then iterates through this array generating a link for each entry. Now I want to add a function for each link that will run a function that does something with this data. The problem is that the data seems to be unavailable after the ajax success function is called (although I thought that they behave like closures). What is the best way to use the queried json data later on? (I think setting it as a global variable would do the job, but I want to avoid that, mainly because this ajax request might be called multiple times)
Thanks.
Your problem is that the i variable is shared by the callbacks.
Therefore, all of the callbacks run on the last item.
The simplest solution is to use $.each:
$.each(data, function(i) {
$("#Data_" + i).click(function() {
alert(data[i]);
RunFunction(data[i]);
return false;
});
});
This will make a separate function call for each iteration, so there will be a separate i variable (or, in this case, parameter) for each iteration.
You can use .bind() directly and passing the data:
for (var i = 0; i< data.length; i++) {
$("#Data_"+i).bind('click', {data: data[i]}, function() {
alert(event.data.data);
RunFunction(event.data.data);
return false;
});
}
I think you made a classical mistake, trying to generate functions in a loop. The variable i will have the same value for all functions but it is not even a valid array index anymore at the end of the loop.
See also JavaScript Closures for Dummies (no offense), example 5.
SLaks answer is a good one, but he failed to explain why it wasn't working.
The problem is due to scoping. Try this out:
var logger = function(x){
console.log(x);
};
for(var j = 0; j < 10; j++){
window.setTimeout(function(){
logger(j);
}, 1000);
}
That nice little function prints out nothing but...9s! That's because the reference to j is kept by the timeout, so by the time the timeout runs, j is already set to 9.
Contrast that with:
var logger = function(x){
console.log(x);
};
for(var j = 0; j < 10; j++){
// we're wrapping our call in an anonymous function
// don't do this in production code...make the function external instead
// so you don't create 10 functions
(function(k){
window.setTimeout(function(){
logger(k);
}, 1000);
})(j);
}
This version wraps the inner call in an anonymous function that takes as an argument the index. Since the scope of k is now limited to that function, the logger works as you'd expect.

Categories