3

For some reason, I cannot loop collection.count. It keeps printing filename[5] 5 times instead of starting at 1 and going to 5.

This makes no sense because I can manually copy and paste this function 5 times and it will work, but when in a for loop, it doesn't.

for( var k =0; k<(filename.length)-2;k++) {

    collection.count( { "display.Name": filename[k] } , function(err, count) {
        console.log("Filename: " +filename[k]);
        console.log(" Trips: " + count);

    }); 

} 

NOTE: If I put the console.log outside of collection.count, I will see all 5 of my filenames. It is only within the specific function that it doesn't work.

Stennie
  • 63,885
  • 14
  • 149
  • 175
krikara
  • 2,395
  • 10
  • 37
  • 71

1 Answers1

4

Let me try to explain this without spewing word soup and throwing out lots of fun terms like "asynchronous", "scope chain", or "closure".

The problem occurs because your loop finishes long before even the first MongoDB query returns. By the time your callback is running, the variable k has been through the loop 5 times and now equals 5. So by the time all five of your callbacks come back console.log("Filename: " + filename[k]); is going to be console.log("Filename: " + filename[5]); every single time.

The right queries are being made because a new query begins on each iteration of the loop and uses k while it's at the correct value; however, the variable k is defined in the scope above your callback so when the callbacks finally fire, k will be long finished with being incremented to 5.

Try this instead:

for( var k =0; k<(filename.length)-2;k++) {
    (function (k) {
        collection.count( { "display.Name": filename[k] } , function(err, count) {
            console.log("Filename: " +filename[k]);
            console.log(" Trips: " + count);
        }); 
    })(k);
}

Now it will work because we've created a new scope around the variable k. All that means in layman's terms is that technically you have two variables named k. The first one is the one you define in the for loop and the second one is the one that is created when the self-invoking function invokes itself, passing in k. The function's argument is also called k but it's technically a brand new variable definition. That means that the second k variable will NOT change on each iteration of the loop, only the one in the outer scope is changing.

A new anonymous function is defined on each iteration and k is passed into it. Now when the callbacks fire, each of their k variables will be unique and will not be the same as the outer k variable.

To make things easier to understand just change the variable name in the anonymous function so it's not the same as the outer one and then you'll see why this works.

for( var k =0; k<(filename.length)-2;k++) {
    (function (x) {
        collection.count( { "display.Name": filename[x] } , function(err, count) {
            console.log("Filename: " +filename[x]);
            console.log(" Trips: " + count);
        }); 
    })(k);
}

Another fancy way to accomplish the same thing is to only wrap the callback function in a new scope instead of the whole query. Though I'll leave it up to you to decide if it really is easier to read or not :P

for( var k =0; k<(filename.length)-2;k++) {
    collection.count( { "display.Name": filename[k] } , (function (x) {
        return function(err, count) {
            console.log("Filename: " +filename[x]);
            console.log(" Trips: " + count);
        };
    })(k)); 
}

In this one the self-invoking function is supplied as the second argument to collection.count. The self-invoking function is passed k, which becomes x inside the self-invoking function. It immediately returns another function; the function that will actually be passed as the second argument to collection.count. The self-invoking function becomes kind of a mini factory that returns another function which can now reference the variable defined in the outer scope (the scope inside the self-invoking function), which we know won't change because technically there is a new anonymous function (complete with x argument definition) being defined on every single iteration.

Put another way, I'm sure you understand that the callback function you defined is being defined five times; once for every iteration of the loop. Well the same goes for the anonymous self-invoking function. It too is created five separate times, including it's argument which is now locked in to whatever the value of k was at the time the function was invoked.

Hopefully that makes sense. It's not a complicated concept so much as it's complicated to try to explain clearly.

CatDadCode
  • 58,507
  • 61
  • 212
  • 318
  • Excellent explanation. Perhaps add this alternative as well. Instead of creating the function k or X, simply do `var vIndex=0;` along with `console.log("Filename: " +filename[vIndex]); vIndex++;` – krikara Jan 24 '14 at 05:36
  • 1
    Yeah I suppose except it's an extra unnecessary variable being incremented and it wouldn't work if the variable you needed was anything more complicated than an integer count. You also wouldn't have learned anything new if I just fixed your code with a hack. Teach a person to fish ;) – CatDadCode Jan 24 '14 at 06:05
  • Yeah, I see what you mean. As soon as I started doing more than count, the index proved to be unreliable. – krikara Jan 24 '14 at 08:17
  • 1
    Ah yes, that is something I should have thought of to tell you. Your callbacks do *not* have to be called in the same order you made the original query. The whole point of passing in a callback is to give mongoose a function that it can invoke when its all finished. This way mongoose/mongodb can take as long as it needs with each query without forcing your app to wait. You are never guaranteed that the callbacks will fire in the order they were defined; if you were then you wouldn't need callbacks because you could just do `var count = collection.count({ "display.Name": filename[k] });` – CatDadCode Jan 24 '14 at 15:18