Why Does a Module Level Return Statement Work in Node.Js

Why does a module level return statement work in Node.js?

TL;DR

The modules are wrapped by Node.js within a function, like this:

(function (exports, require, module, __filename, __dirname) {
// our actual module code
});

So the above shown code is actually executed by Node.js, like this

(function (exports, require, module, __filename, __dirname) {
console.log("Trying to reach");
return;
console.log("dead code");
});

That is why the program prints only Trying to reach and skips the console.log following the return statement.

Internals

This is where we need to understand how Node.js processes Modules. When you run your .js file with Node.js, it treats that as a module and compiles it with the v8 JavaScript engine.

It all starts with runMain function,

// bootstrap main module.
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};

In the Module._load function, a new Module object is created and it is loaded.

var module = new Module(filename, parent);
...
...
try {
module.load(filename);
hadException = false;

The Module function's load does this,

// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
debug('load ' + JSON.stringify(filename) +
' for module ' + JSON.stringify(this.id));

assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));

var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};

Since our file's extension is js, we see what the Module._extensions has for .js. It can be seen here

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};

The module object's _compile is invoked in that function and this is where the magic happens,

// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.

This is where the require function, used by our node modules's is created first.

function require(path) {
return self.require(path);
}

require.resolve = function(request) {
return Module._resolveFilename(request, self);
};

Object.defineProperty(require, 'paths', { get: function() {
throw new Error('require.paths is removed. Use ' +
'node_modules folders, or the NODE_PATH ' +
'environment variable instead.');
}});

require.main = process.mainModule;

// Enable support to add extra extension types
require.extensions = Module._extensions;
require.registerExtension = function() {
throw new Error('require.registerExtension() removed. Use ' +
'require.extensions instead.');
};

require.cache = Module._cache;

And then there is something about wrapping the code,

// create wrapper function
var wrapper = Module.wrap(content);

We set out to find what Module.wrap does, which is nothing but

Module.wrap = NativeModule.wrap;

which is defined in src/node.js file and that is where we find this,

NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

This is how our programs have access to the magic variables, exports, require, module, __filename and __dirname

Then the wrapped function is compiled and executed here with runInThisContext,

var compiledWrapper = runInThisContext(wrapper, { filename: filename });

And then finally, the module's compiled wrapped function object is invoked like this, with values populated for exports, require, module, __filename and __dirname

var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);

This is how our modules are processed and executed by Node.js and that is why the return statement works without failing.

Use of return keyword in Javascript scripts out of functions

This woke my curiosity upon what else does returning in the middle of a Node module, apart from terminating execution, does.

When you run a Javascript script file with node.js, it is wrapped in a module wrapper function as you can see here in the node.js docs for Modules.

(function (exports, require, module, __filename, __dirname) {
// Your module code actually lives in here
});

So, your return is a return from that module wrapper function and it does nothing except stop the execution of any further code in that module. Modules in node.js don't have any documented behavior for returning a value from the module. So, return; or return 10; or no return value at all, all have the same behavior. The return value is not used.

As always in a Javascript function, you could use a plain return to skip the execution of the rest of the code in the module, though it is probably better to just use an if/else to more clearly execute only the code you want to execute.

My expectation was that this would make the return status of the process be equal to 10. However, upon asking the return status of the last utility ran under bash, by echo $?, I would receive 0, so that is not the case.

If you want to set a return value for the process, you should use:

process.exit(10);

In node modules, if you want to share data with other modules, you can use module.exports = some object or value, but that has no effect on the main module since the node loader is not paying any attention to either the return value of the module.exports from the main module. That is only useful with other modules that you explicitly require() in.

Also, I wonder what happens in the context of browser <script>s.

Using a return statement at the global level (which is what the top level of a browser <script> tag would be is not permitted by the language. return is only appropriate inside a function. Doing so generates this error:

Uncaught SyntaxError: Illegal return statement

What is the purpose of Node.js module.exports and how do you use it?

module.exports is the object that's actually returned as the result of a require call.

The exports variable is initially set to that same object (i.e. it's a shorthand "alias"), so in the module code you would usually write something like this:

let myFunc1 = function() { ... };
let myFunc2 = function() { ... };
exports.myFunc1 = myFunc1;
exports.myFunc2 = myFunc2;

to export (or "expose") the internally scoped functions myFunc1 and myFunc2.

And in the calling code you would use:

const m = require('./mymodule');
m.myFunc1();

where the last line shows how the result of require is (usually) just a plain object whose properties may be accessed.

NB: if you overwrite exports then it will no longer refer to module.exports. So if you wish to assign a new object (or a function reference) to exports then you should also assign that new object to module.exports


It's worth noting that the name added to the exports object does not have to be the same as the module's internally scoped name for the value that you're adding, so you could have:

let myVeryLongInternalName = function() { ... };
exports.shortName = myVeryLongInternalName;
// add other objects, functions, as required

followed by:

const m = require('./mymodule');
m.shortName(); // invokes module.myVeryLongInternalName

What does this mean in a nodejs module?

this (in the context of a module) is the same as exports in node.js. However you should generally use exports/module.exports instead, so that it's explicitly clear what you're modifying.

Meaning of this in node.js modules and functions

Here's a few fundamental facts you must understand to clarify the situation:

  • In the top-level code in a Node module, this is equivalent to module.exports. That's the empty object you see.

  • When you use this inside of a function, the value of this is determined anew before each and every execution of the function, and its value is determined by how the function is executed. This means that two invocations of the exact same function object could have different this values if the invocation mechanisms are different (e.g. aFunction() vs. aFunction.call(newThis) vs. emitter.addEventListener("someEvent", aFunction);, etc.) In your case, aFunction() in non-strict mode runs the function with this set to the global object.

  • When JavaScript files are required as Node modules, the Node engine runs the module code inside of a wrapper function. That module-wrapping function is invoked with a this set to module.exports. (Recall, above, a function may be run with an abitrary this value.)

Thus, you get different this values because each this resides inside a different function: the first is inside of the Node-created module-wrapper function and the second is inside of aFunction.

Node.js modules VS IIFE functions' scope

The scopes that a function has access to depend on where the function is declared, not where the function is called.

The two JS modules are different scopes. The function you create in test2 doesn't have access to any variables declared in test1.

requireing a module makes its exports available in test1, but it doesn't change which scopes it has access to.

If you want to use data from test1 in the exported function, you'll need to change that function to accept an argument and then pass it.

module.exports vs exports in Node.js

Setting module.exports allows the database_module function to be called like a function when required. Simply setting exports wouldn't allow the function to be
exported because node exports the object module.exports references. The following code wouldn't allow the user to call the function.

module.js

The following won't work.

exports = nano = function database_module(cfg) {return;}

The following will work if module.exports is set.

module.exports = exports = nano = function database_module(cfg) {return;}

console

var func = require('./module.js');
// the following line will **work** with module.exports
func();

Basically node.js doesn't export the object that exports currently references, but exports the properties of what exports originally references. Although Node.js does export the object module.exports references, allowing you to call it like a function.



2nd least important reason

They set both module.exports and exports to ensure exports isn't referencing the prior exported object. By setting both you use exports as a shorthand and avoid potential bugs later on down the road.

Using exports.prop = true instead of module.exports.prop = true saves characters and avoids confusion.

Does Importing(require) module in JS also executes whole code as well?

Yes. The first time a CommonJS module is loaded with require(), any top level code is executed. As you can see, this has to be the case so that exports.calcModule = calc runs and establishes the exports for the module and your console.log('hellow from module') would also run.

Once loaded, the module is cached so any other calls to require() for that same module will just return the export object from the original execution of the module and the top level code will not run again. So, the top level code only runs once, no matter how many times the module is loaded within your program.

does importing a module includes executing whole module file?

Yes, it executes all top level code in the module you are loading.



Related Topics



Leave a reply



Submit