Reza
Reza

Reputation: 3

Node.js calling a callback function inside a callback

I started learning node.js by reading Node JS in Action book recently. This is probably a newbie question, but after reading several posts of callback functions and javascript scope of variables, I still have problem understand the idea behind this code in chapter 5 of the book.

function loadOrInitializeTaskArray(file, cb) {
  fs.exists(file, function(exists) {
    var tasks = [];
    if (exists) {
      fs.readFile(file, 'utf8', function(err, data) {
        if (err) throw err;
        var data = data.toString();
        var tasks = JSON.parse(data || '[]');
        cb(tasks);
      });
    } else {
      cb([]); 
    }
  });
}

function listTasks(file) {
  loadOrInitializeTaskArray(file, function(tasks) {
    for(var i in tasks) {
      console.log(tasks[i]);
    }
  });
}

It includes three callback functions divided in two functions. listTasks(..) is called first and it calls loadorInitializeTaskArray(.. ) later. My problem starts here, how is this call handle by node? loadOrInitializeTaskArray takes two arguments and the second one is the callback function which shouldn't accept any parameters according to is signature but it does!!
when does cb(..) get called in loadorInitializeTaskArray and what is that (the same function that calls helper function)?

"tasks" is an array declared inside function loadOrInitializeTaskArray, how do we have access to it in listTasks(..) function?

I know in Javascript, scope of a variable is inside the function it define and all nested functions. But I have a hard time understanding it here. So can someone explain what is going on here? Thank you

Upvotes: 0

Views: 7306

Answers (3)

Vishal
Vishal

Reputation: 166

You really are having a hard time in understanding the scope of variable inside the nested functions. So, lets start with it.
Let's consider this code

function foo(){
  var a = 3;
  function bar(){
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // the value of a i.e 3 will appear on console

If you know langauges like C, C++... Your brain would be interpreting this code like this. If you don't know any of them just ignore thiese points.

  • When foo() is called, variable a will be declared and assigned value 3.
  • When foo is returned the stack containing a will be destroyed. And hence a would be destroyed too.
  • Then how in the world baz() is outputting 3. ???

Well, in javascript when a function is called the things that happen are different than in C. So, first let your all C things go out from your mind before reading further.

In javascript scope resolution is done by travelling down a chain of objects that defines variables that are "in scope" for that code. Let's see how?

  • When you declare a global JavaScript variable, what you are actually doing is defining a property of the global object.
  • In top-level JavaScript code (i.e., code not contained within any function definitions), the scope chain consists of a single object, the global object.
  • In a non-nested function, the scope chain consists of two objects. The first is the object that defines the function’s parameters and local variables, and the second is the global object.
  • In a nested function, the scope chain has three or more objects. When a function is defined, it stores the scope chain then in effect. When that function is invoked, it creates a new object to store its local variables, and adds that new object to the stored scope chain to create a new, longer, chain that represents the scope for that function invocation.

So, when foo() is executed. It creates a new object to store it's local variables. So, variable a will be stored in that object. And also, bar is defined. When bar is defined it stores the scope chain in effect. So, the scope chain of bar now contains the object that has variable a in it. So, when bar is returned it has a reference to it's scope chain. And hence it knows a.

So, I guess it answers your question how node handles the code.

You wrote:

loadOrInitializeTaskArray takes two arguments and the second one is the callback function which shouldn't accept any parameters according to is signature but it does!!

The callback function is

function(tasks) {
    for(var i in tasks) {
      console.log(tasks[i]);
    }
}

And it accepts an argument tasks. So, you are wrong here.

And When loadOrIntializeTaskArray is called cb refers to this call back function. And cb(arg) basically does this tasks = arg where tasks is the argument in callback function.

I guess you still will be having a lot of questions. Let me know in comments. And I highly recommend you to go through the Core Javascript before diving into node.

Upvotes: 3

mscdex
mscdex

Reputation: 106698

I'm not sure what you mean by "the second one is the callback function which shouldn't accept any parameters according to is signature". The callback signature (function(tasks)) certainly does expect an argument (tasks).

cb is the callback function that was passed in. In this case it is literally the anonymous function:

function(tasks) {
  for(var i in tasks) {
    console.log(tasks[i]);
  }
}

tasks is the name of two different variables (but they point to the same object because arrays are passed by reference) in different scopes. One is defined in loadOrInitializeTaskArray() as you noted, the other is a parameter for the callback passed to loadOrInitializeTaskArray().

Also, on an unrelated note, typically callbacks follow the "error-first" format. This allows for handling of errors upstream. Modifying the code to follow this convention would result in:

function loadOrInitializeTaskArray(file, cb) {
  fs.exists(file, function(exists) {
    var tasks = [];
    if (exists) {
      fs.readFile(file, 'utf8', function(err, data) {
        if (err)
          return cb(err);

        var data = data.toString();

        // JSON.parse() throws errors/exceptions on parse errors
        try {
          var tasks = JSON.parse(data || '[]');
        } catch (ex) {
          return cb(ex);
        }

        cb(null, tasks);
      });
    } else {
      cb(null, []); 
    }
  });
}

function listTasks(file) {
  loadOrInitializeTaskArray(file, function(err, tasks) {
    if (err) throw err;
    for (var i in tasks) {
      console.log(tasks[i]);
    }
  });
}

Upvotes: 0

gruntledcustomer
gruntledcustomer

Reputation: 253

First, there is not really any such thing as a function signature in javascript. You can pass as many or as few arguments to a function as you like. The second argument to loadOrInitialiseTaskArray is simply assigned to the local variable cb when the function is called. The line cb(tasks) then invokes this value, so the second argument had better have been a function.

When you call loadOrInitializeTaskArray from listTasks, the second argument is indeed a function, and the first argument to this function is named tasks in its own scope. It isn't reaching into loadOrInitializeTaskArray and using the variable declared in that function's scope. You explicitly passed in that value when you invoked cb.

The code would work the same and perhaps be easier to understand if we renamed the variables:

function loadOrInitializeTaskArray(file, cb) {
  fs.exists(file, function(exists) {
    var taskArray = [];
    if (exists) {
      fs.readFile(file, 'utf8', function(err, data) {
        if (err) throw err;
        var data = data.toString();
        var taskArray = JSON.parse(data || '[]');
        cb(taskArray);
      });
    } else {
      cb([]); 
    }
  });
}

function listTasks(file) {
  loadOrInitializeTaskArray(file, function(listOfTasks) {
    for(var i in listOfTasks) {
      console.log(listOfTasks[i]);
    }
  });
}

Upvotes: 0

Related Questions