CommonJS
在node.js
中,使用 CommonJS 作为其模块化规范。
根据 CommonJS 模块化规范,一个单独的JS文件就是一个模块,每个模块是一个单独的作用域
,在模块内部定义的变量,无法在其他模块中所读取。若想要在模块间进行通信,需使用模块的导出与导入语法:
- 模块通过变量
exports
向外暴露API
,注意,exports
只能是一个对象,该对象的属性即为向外暴露的API
。 - 在另一个模块中,通过全局函数
require
引入其他模块导出的exports
对象。
基本语法
例如,a.js
文件中导出对象exports
,其中包含了name
、add
属性,分别是文件中定义的常量。
const name = 'Jack';
const add = (a, b) => a + b;
module.exports = {
name,
add
};
在文件b.js
中,通过全局函数require
引入该导出的对象exports
。
const api = require('./a');
console.log(api);
// { name: 'Jack', add: [Function: add] }
注意,api
这个可以随意取的。通常,也可以通过解构赋值的形式,按需进行导入:
const { name } = require('./a');
console.log(name);
// Jack
多次导入
接上述例子,若在b.js
文件中,多次导入a
模块,会怎么样呢?两次导入的是同一个对象吗?
const api1 = require('./a');
const api2 = require('./a');
console.log(api1 === api2);
// true
如上述代码所示,b.js
中两次引入a
模块,并判断两个导入的变量是否为相等。结果输出true
,说明若多次引用,实际上导入的模块是同一个对象。
实际上,在第一次导入中,会执行要导入的文件,并在内存中缓存一个对象,其中exports
就是要导入的对象。再次导入相同模块时,并不会再执行了,会直接从内存中取这个exports
对象。
{
id: '...', // 模块名
exports: { ... }, // 模块输出的接口
loaded: true, // 模块的脚本是否执行完毕
...
}
是否支持更改
导入模块,实际上就是执行一遍要导入的模块,然后将其输出的exports
对象,作为require
函数的返回值。实际上就是普通的赋值语句,若使用var
、let
进行声明,当然支持更改。
let api1 = require('./a');
api1 = {};
console.log(api1);
// {}
循环引用
若出现某模块被循环加载,只输出已经执行的部分,未执行的不会输出。
若在a.js
中引入b.js
,在b.js
中引入a.js
,形成循环引用,是否会报错呢?若不报错,运行的结果是怎么样的?
还是以代码举例比较清晰。如下代码所示,a
模块引入b
模块,b
模块引入a
模块。
// a.js
const b = require('./b');
console.log('b', b);
const name = 'Jack';
const age = 18;
module.exports = {
name,
age
};
// b.js
const a = require('./a');
console.log('a', a);
const id = '001';
module.exports = {
id
};
运行node a.js
,控制台输出如下:
a {}
b { id: '001' }
没有报错,正常运行。这是因为前面提到的原因:若出现某模块被循环加载,只输出已经执行的部分,未执行的不会输出。
简单分析一下:
- 运行
node a.js
。首先进入到a
模块,a
模块一开始就导入b
模块,那么就会先执行一遍b.js
,然后返回b.js
导出的exports
对象给a
模块中的常量b
。 - 执行
b.js
。一开始又导入了a.js
。但是a.js
并未执行完毕,只会返回已执行的部分。由于a.js
导出的是name
、age
,此时a.js
只执行到第1
行,所以实际上此时导入a.js
,只会返回空对象,因为name
、age
还未执行到。所以require('./a')
返回空对象,赋值给常量a
,故打印出空对象。 - 接着继续执行
b.js
,将剩下的代码执行完毕,正常导出了exports
对象,其中包含了id
属性。 - 执行完毕后,又回到了
a.js
的第一行中,require('./b')
返回b
导出的exports
对象,包含了id
属性。所以,下面能正常打印出{ id: '001' }
。 - 接着在
a.js
中,将剩余的代码执行完毕。
思考题
有a.js
、b.js
、c.js
三个文件,执行node a.js
,控制台将会输出什么?
// a.js
const b = require('./b');
console.log(exports.x);
exports.x = 'x';
require('./c');
// b.js
const a = require('./a');
console.log(a);
a.x = 'y';
// c.js
const a = require('./a');
console.log(a.x);
答案:
{}
y
x
以上为本人学习所得,若有不妥,欢迎批评指出!