Reputation: 3
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
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.
foo()
is called, variable a
will be declared and assigned value 3. foo
is returned the stack containing a will be destroyed. And hence a would be destroyed too.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?
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
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
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