0

I do have an issue with scoping variables in Javascript (Node.js / Express)

Can someone please explain to me why is the "out" variable not being scoped to the last line in the following script?

exports.last = function(req, res){
  var out = [];
  var q = Point.find({ token: req.params.token }).distinct('key');
  q.exec(function (err, keys, count) {
    keys.forEach(function(k) {
      var q2 = Point.findOne({ token: req.params.token, key: k }).sort({time:-1})
      q2.exec(function(err, p, count) {
        out.push({ a: Date.parse(p.time), b: parseFloat(p.val) });
      });
    })
    console.log(out);
  })
  res.json(out);
};
MarcelH
  • 43
  • 5

1 Answers1

2

The commentors are correct: this is not a scoping issue, but a consequence of the asynchronous nature of JavaScript. Imagine you could assign an "step number" to every line of code. Here is a simplified view of your code (the numbers are step numbers, as the JavaScript engine executes things, NOT line numbers):

1   var thing = [];
2   q2.exec(function(){
        // some stuff happens
15      thing.push('hello!');
    });
3   console.log(thing);  // still empty!

See how the flow goes 1, 2, 3, skipping over the function call in q2.exec? That's because q2.exec is asynchronous: you're saying "whenever q2.exec is done, call this function." You don't know when it's going to be done. So it proceeds on to step 3, and then it does other stuff...eventually, q.exec finishes, and it executes your function (in this fictional example, it's step 15).

Hope that clarifies things.

As far as how to fix it, you'll have to either employ callbacks or, more likely in this case, use "promises". Why promises in this case? Because you're calling q2.exec a bunch of times, and I assume you only want to log to the console after all the q2.exec calls have completed. So you could do something like this (using the Q promises library):

var Q = require('q');  // this has nothing to do with your 'q2'
var promises = [];
var thing = [];
stuff.forEach(function(x,i){
    var deferred = Q.defer();
    q2.exec(function(){
        thing.push('hello ' + i);
        deferred.resolve();
    });
    promises.push(deferred.promise);
});
Q.all(promises).then(function(){
    // all calls to q2.exec now complete
    console.log(thing);
});
Ethan Brown
  • 26,892
  • 4
  • 80
  • 92
  • 1
    Promises are being added to pure JavaScript. Just wanted to get that out there. – TheThirdOne Feb 07 '14 at 01:59
  • One day, yes! It'll probably be available in Node soon, but you won't be able to rely on it in browsers without polyfilling for another couple of years, probably :( – Ethan Brown Feb 07 '14 at 02:01
  • I just read a good article about that exact topic, incidentally: http://www.html5rocks.com/en/tutorials/es6/promises/ – Ethan Brown Feb 07 '14 at 02:03
  • Chrome lets you enable built-in Promises already, but it is still experimental. – TheThirdOne Feb 07 '14 at 02:05
  • I don't even have to enable them...they're there by default in Chrome (32). I hear Firefox has them too. My point, though, is if you're developing "real world" website, you have to be prepared for people to use older browsers. Sad but true. – Ethan Brown Feb 07 '14 at 02:08