Why does Node.js appear to pass arguments differently in these two uses?
Check out this super-simple node.js program:
var g = { a : 1, b : 2 }
function callBack (key, value) {
console.log("Callback called with key: " + key + "\nAnd value: " + value) ;
}
function doNothing (key, value, cb) {
true ;
console.log(key + ": doing nothing") ;
cb() ;
}
function doLoop () {
for (k in g) {
f = function () {
callBack(k, g[k]) ;
}
doNothing(k, g[k], f) ;
}
}
doLoop() ;
When run, it produces this output:
a: doing nothing
Callback called with key: a
And value: 1
b: doing nothing
Callback called with key: b
And value: 2
OK. That makes sense - each time the callback is called, it has the correct arguments.
Now look at this program:
var mysql = require('mysql') ;
var dbClient = undefined ;
var db_uri = "mysql://xxx:[email protected]/xxx" ;
var schema = {
redirects : "(id int AUTO_INCREMENT, key VARCHAR(50), url VARCHAR(2048))",
clicks : "(ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, IP VARBINARY(16))"
} ;
function createOnEmpty(err, results, fields, tableName, create_def) {
console.log("createOnEmpty called on " + tableName) ;
if (err) {
console.error(err) ;
process.exit(1) ;
} else {
if (0 == results.length) {
dbClient.query(["create table ", tableName, create_def].join(" "),
function (err, results, fields) {} ) ;
} else {
console.log(tableName + " table already exists.") ;
}
}
console.log("\n\n") ;
}
function setupSchema() {
for (table in schema) {
console.log("Checking for table: " + table) ;
// FIXME: Why does this always seem to pass clicks as tablename?!
dbClient.query(
"show tables LIKE '" + table + "'",
function (err, results, fields) {
createOnEmpty(err, results, fields, table, schema[table])
}
);
}
}
function handleDBConnect(err) {
if (err) {
console.error("ERROR: problem connecting to DB: " + err.code) ;
process.exit(1) ;
} else {
console.log("Connected to database.") ;
// Automatically set up the schema, if the tables don't exist
setupSchema() ;
}
}
function MySQLConnect() {
dbClient = mysql.createConnection(db_uri) ;
dbClient.connect(handleDBConnect) ;
}
MySQLConnect() ;
It outputs:
Connected to database.
Checking for table: redirects
Checking for table: clicks
createOnEmpty called on clicks
createOnEmpty called on clicks
The loop seems to be giving the argument 'clicks' as the argument 'table' both times, even though the variable has clearly been switched to 'redirects'.
I figure I must have some fundamental misunderstanding of how JavaScript/Node works here.
Answer
To understand this behaviour you need to understand this 2 core js concepts:
- Closures
- Eventloop
Let say we have global variable a
, log
function which outputs a
to console, and main function which calls log
twice, first time with timeout (asynchronously), second time just simple function call
var a = 42;
function log() {
console.log(`a is ${a}`);
}
function main() {
setTimeout(log, 100);
a = 13;
log();
}
main();
This code produces the following output:
a is 13
a is 13
Why the hell the first time a is 13?
When you call setTimeout, it doesn't block main js thread for 100ms, it just adds log function to callback queue. The next line is a = 13
. As a
wasn't declared inside function body with var
keyword, 13 assigned to a
which was declared on the first line of code. Then we have the first line of output as a result of the last line of main
function. Now we have empty callstack, nothing else happening in our code, but we still have log function in callback queue. After 100ms passed, if and only if callstack is empty (which is our case), log
function could be called second time. It logs 'a is 13'
once again, as a
-s value was already reassigned.
This is a short explanation of how async callbacks work in javascript, and this is the reason why createOnEmpty called on clicks
twice. dbClient.query
is asynchronous and by the time it was called first time, your for loop finished its execution and table
value is clicks
.
Quick and dirty solution of your problem will be
for (table in schema) {
console.log("Checking for table: " + table) ;
(function (table) {
dbClient.query("show tables LIKE '" + table + "'",
function (err, results, fields) {
createOnEmpty(err, results, fields, table,
schema[table]) } );
)(table);
}
This immediately called function memorizes table
value on each loop iteration in scope
- Check out this excellent talk about eventloop and how async stuff works in js
- How closures work
Related Questions
- → How to update data attribute on Ajax complete
- → October CMS - Radio Button Ajax Click Twice in a Row Causes Content to disappear
- → Octobercms Component Unique id (Twig & Javascript)
- → Passing a JS var from AJAX response to Twig
- → Laravel {!! Form::open() !!} doesn't work within AngularJS
- → DropzoneJS & Laravel - Output form validation errors
- → Import statement and Babel
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM