Overview
If I may say so myself, the struggle to understand the exports in CommonJS is all due to the poor way internet articles are written.
No offense, I always appreciate the help I get from online articles. But the way they describe the exports in CommonJS is appalling. I believe my explanation is the clearest in the world!
The so-called "two ways to export in CommonJS"
// Firstly, module.exports.
// Common explanation: The importer (require) can freely change the name with this.
module.exports = (i) => i + 1;
// Common explanation: Use this exports to avoid free name changes.
exports.increment = (i) => i + 1;
This phrase like "Use exports
instead of module.exports
". As exports
is a shortcut for module.exports
, it promotes the misunderstanding that they are different things even though they are the same.
Here's How CommonJS Exports are Understood by Mr. Green
- In the CommonJS runtime environment, a set of "module scope variables" are automatically created for each file. They're similar to built-in variables in Python.
- You can find a list here: https://nodejs.org/api/modules.html#the-module-scope.
- The module scope variables relevant to this matter are the following three:
module
: This is an object that contains a property namedexports
(which is an empty object).exports
: This is a reference tomodule.exports
↑.require
: Basically, this is a function that refers tomodule.exports
from a different file.
"In the CommonJS runtime environment, you can access the module.exports
of another file using require()
". In my understanding, this is the entirety of how exports and imports work in CommonJS. There are not two ways to do this.
The Following are Experiment Notes
I'll leave behind some notes on the experiments that led me to this understanding.
node -v
# v20.3.0
# This is like python -c in Python.
node --eval 'console.info({ module })'
# {
# module: Module {
# id: '[eval]',
# path: '.',
# exports: {},
# filename: '/path/to/here/[eval]',
# loaded: false,
# children: [],
# paths: [
# ...
# '/node_modules'
# ]
# }
# }
As you can see, in the Node.js (CommonJS) runtime environment, the module scope variable module
is automatically defined. It's somewhat akin to __name__
in Python.
Now, what's making things complicated is the existence of the module scope variable exports
. However, this guy is just a reference to module.exports
. Let's take a look at it.
node --eval 'module.exports.foo = 1; exports.bar = 2; console.info({ exports })'
# { exports: { foo: 1, bar: 2 } }
See? Personally, I think it's better to always use module.exports
and seal off exports
.
And, require()
simply returns the contents of module.exports
from another file.
echo 'exports.increment = (i) => i + 1; module.exports.sunday = "Sunday";' > foo.js
node --eval 'console.info(require("./foo.js"))'
# { increment: [Function (anonymous)], sunday: 'Sunday' }
Therefore, the idea that there are two ways to export in CommonJS is a misconstrued notion. The default require
function can access the module.exports
property of the default module
object in another file. That's all there is to it.