6

I feel this should be answered somewhere in the internet but I failed to find it, maybe because I'm not searching the correct terms but this is the problem: I have the following function:

function ParentFunction (DataBase, Parameters) {            
  for (k = 0; k < DataBase.length; k++){
    var CalendarURL = "https://www.google.com/calendar/feeds/" + DataBase.cid;
    $.ajax({
      url: CalendarURL,
      dataType: 'json',
      timeout: 3000,
      success: function( data ) { succesFunction(data, k, Parameters);},
      error: function( data ) { errorFunction ("Error",Parameters); }
    });
  }
}

I was getting errors in succesFunction(data, k, Parameters) because 'k' was always evaluated with the latest value. What is happening is that, when the for loop runs k is correctly increased but, when the callback function successFunction was executed, typically several ms after the loop was finished, it was always been evaluated with the last value of k, not the value of the loop the $.ajax was called.

I fixed this by creating another function that contains the ajax call. It looks like this:

function ParentFunction (DataBase, Parameters) {        
  for (k = 0; k < DataBase.length; k++){
    var CalendarURL = "https://www.google.com/calendar/feeds/" + DataBase.cid;
    AjaxCall(CalendarURL, k, Parameters);
  }
}

function AjaxCall(URL, GroupIndex, Parameters) {
    $.ajax({
      url: URL,
      dataType: 'json',
      timeout: 3000,
      success: function( data ) { succesFunction(data, GroupIndex, Parameters);},
      error: function( data ) { errorFunction ("Error",Parameters); }
    });
}

and it works. I think when the function is called in the parentFunction a copy of the value of the arguments is created and when the callback executes sees this value instead of the variable k which by the time would have a wrong value.

So my question is, is this the way to implement this behaviour? Or is there more appropriate way to do it? I worry that either, different browsers will act differently and make my solution work in some situations and not work in others.

Michał
  • 2,456
  • 4
  • 26
  • 33
Thelemitian
  • 355
  • 1
  • 14
  • 2
    That's the way. In the future we will get [block scoped bindings](http://wiki.ecmascript.org/doku.php?id=harmony:block_scoped_bindings). – Felix Kling Mar 09 '14 at 20:19

3 Answers3

3

You are hitting a common problem with javascript: var variables are function-scoped, not block-scoped. I'm going to use a simpler example, that reproduces the same problem:

for(var i = 0; i < 5; i++) {
  setTimeout(function() { alert(i) }, 100 * i);
}

Intuitively, you would get alerts of 0 through 4, but in reality you get 5 of 5, because the i variable is shared by the whole function, instead of just the for block.

A possible solution is to make the for block a function instead:

for(var i = 0; i < 5; i++) {
  (function(local_i) {
    setTimeout(function() { alert(local_i); }, 100 * i);
  })(i);
}

Not the prettiest or easier to read, though. Other solution is to create a separate function entirely:

for(var i = 0; i < 5; i++) {
  scheduleAlert(i);
}

function scheduleAlert(i) {
  setTimeout(function() { alert(i); }, 100 * i);
}

In the (hopefully near) future, when browsers start supporting ES6, we're going to be able to use let instead of var, which has the block-scoped semantics and won't lead to this kind of confusion.

Renato Zannon
  • 28,805
  • 6
  • 38
  • 42
1

Another option – rather than creating a new named function – would be to use a partial application. Simply put, a partial application is a function that accepts a function that takes n arguments, and m arguments that should be partially applied, and returns a function that takes (n - m) arguments.

A simple implementation of a left-side partial application would be something like this:

var partial = (function() {
  var slice = Array.prototype.slice;
  return function(fn) {
    var args = slice.call(arguments,1);
    return function() {
      return fn.apply(this, args.concat(slice.call(arguments)));
    }
  }
}).call();

With this, then you can take a function that requires two arguments like:

function add(a,b) { return a + b; }

Into a function that requires only one argument:

var increment = partial(add, 1);
increment(1);  // 2
increment(10); // 11

Or even a function that requires no arugments:

var return10 = partial(add, 5, 5);
return10(); // 10

This is a simple left-side only partial application function, however underscore.js provides a version that can partially apply an argument anywhere in the argument list.

For your example, instead of calling AjaxCall() to create a stable variable scope, you could instead do:

function ParentFunction (DataBase, Parameters) {            
  for (k = 0; k < DataBase.length; k++){
    var CalendarURL = "https://www.google.com/calendar/feeds/" + DataBase.cid;
    var onSuccess = _.partial(succesFunction, _, k, Parameters);
    $.ajax({
      url: CalendarURL,
      dataType: 'json',
      timeout: 3000,
      success: onSuccess,
      error: function( data ) { errorFunction ("Error",Parameters); }
    });
  }
}

Here, we are using _.partial() to transform a function with a signature of:

function(data, index, params) { /* work */ }

into a signature of:

function(data) { /* work */ }

Which is the signature that the success callback will actually be invoked with.


Though admittedly, this is all pretty much just syntactical sugar for the same underlying concepts already described, it can sometimes conceptually help to think about problems like these from as functional perspective than procedural one.

J. Holmes
  • 18,466
  • 5
  • 47
  • 52
0

This has to do with closures in javascript. Your anonymous functions each reference a variable outside of their current scope, so each function's "k" is bound to the original looping variable "k." Since these functions are called some time after, each function looks back to see that "k" is sitting at its last value.

The most common way to get around this is exactly what you did. Instead of using "k" in a nested function definition (which forces a closure), you pass it as an argument to an external function, where no closure is needed.

Here are a few posts with similar issues:

How do JavaScript closures work?

JavaScript closure inside loops – simple practical example

Javascript infamous Loop issue?

Community
  • 1
  • 1