views:

461

answers:

1

I have a for loop, and inside it a variable is assigned with var. Also inside the loop a method is called which requires a callback. Inside the callback function I'm using the variable from the loop. I would expect that it's value, inside the callback function, would be the same as it was outside the callback during that iteration of the loop. However, it always seems to be the value from the last iteration of the loop.

Am I misunderstanding scope in JavaScript, or is there something else wrong?

The program in question here is a node.js app that will monitor a working directory for changes and restart the server when it finds one. I'll include all of the code for the curious, but the important bit is the parse_file_list function.

var posix = require('posix');
var sys = require('sys');
var server;
var child_js_file = process.ARGV[2];
var current_dir = __filename.split('/');
current_dir = current_dir.slice(0, current_dir.length-1).join('/');

var start_server = function(){
    server = process.createChildProcess('node', [child_js_file]);
    server.addListener("output", function(data){sys.puts(data);});
};

var restart_server = function(){
    sys.puts('change discovered, restarting server');
    server.close();
    start_server();
};

var parse_file_list = function(dir, files){
    for (var i=0;i<files.length;i++){
        var file = dir+'/'+files[i];
        sys.puts('file assigned: '+file);
        posix.stat(file).addCallback(function(stats){
            sys.puts('stats returned: '+file);
            if (stats.isDirectory())
                posix.readdir(file).addCallback(function(files){
                    parse_file_list(file, files);
                });
            else if (stats.isFile())
                process.watchFile(file, restart_server);
        });
    }
};

posix.readdir(current_dir).addCallback(function(files){
    parse_file_list(current_dir, files);
});

start_server();

The output from this is:

file assigned: /home/defrex/code/node/ejs.js
file assigned: /home/defrex/code/node/templates
file assigned: /home/defrex/code/node/web
file assigned: /home/defrex/code/node/server.js
file assigned: /home/defrex/code/node/settings.js
file assigned: /home/defrex/code/node/apps
file assigned: /home/defrex/code/node/dev_server.js
file assigned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js
stats returned: /home/defrex/code/node/main_urls.js

For those from the future: node.devserver.js

+6  A: 

This is perfectly normal behaviour. Your callbacks are executed at some later point (asynchronously) but still reference the scope your for loop was running in. All callback references to file will therefore have the last value it was set to.

What you want to do is create a new function scope, assign the current value of file to a local variable and create the callback inside that scope.

    for (var i=0;i<files.length;i++){
        var file = dir+'/'+files[i];
        (function() {
            var file_on_callback = file;
            sys.puts('file assigned: '+ file_on_callback);
            posix.stat(file_on_callback).addCallback(function(stats){
                sys.puts('stats returned: '+ file_on_callback);
                if (stats.isDirectory())
                    posix.readdir(file_on_callback).addCallback(function(files){
                        parse_file_list(file_on_callback, files);
                    });
                else if (stats.isFile())
                    process.watchFile(file_on_callback, restart_server);
            });
        })(); // This creates and executes a new function with its own scope.
    }
Michiel Kalkman