一、CommonJS 模块规范
Node 应用由模块组成,采用 CommonJS 模块规范。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
CommonJS 规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,它的 exports
属性(即 module.exports
)是对外的接口。加载某个模块,其实是加载该模块的 module.exports
属性。
CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
CommonJS 模块的特点如下:
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
1. module.exports 属性
module.exports
属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports
变量。
2. exports 变量
为了方便,Node 为每个模块提供一个 exports
变量,它指向 module.exports
。这等同于在每个模块头部,有一行这样的命令:var exports = module.exports
,我们可以通过以下方式来验证
console.log(exports === module.exports); // true
所以在对外输出模块接口时,可以向 exports
对象添加属性方法。
module.exports.age = 20
module.exports.getAge = function() {}
// 相当于
exports.age = 20
exports.getAge = function() {}
但是不能直接将 exports
变量指向一个值,因为这样等于切断了 exports
与 module.exports
的联系。
// 以下写法无效,因为 exports 不再指向 module.exports 了。
exports = function() {};
3. module.exports 与 exports 的使用
当一个模块的对外接口,只是一个单一的值时,不能使用 exports
输出,只能使用 module.exports
输出。
// moduleA.js
// 1️⃣ 正确 ✅
module.exports = function() {};
// 2️⃣ 错误 ❎
exports = function() {};
导入模块看结果:
// other.js
var moduleA = require('moduleA.js');
console.log(moduleA);
// 两种写法打印的值分别为:
// 1️⃣ 预期结果 ✅ ƒ () { console.log('moduleD'); }
// 2️⃣ 非预期结果 ❎ {}
分析结果:
首先我们要知道 module.exports
的初始值是 {}
,当执行 exports = function() {};
赋值时,无论赋值的是基本数据类型还是引用数据类型,都将改变 exports
的指向,即切断了 exports
与 module.exports
的联系。但是我们模块对外输出的接口是 module.exports
,所以 2️⃣ 得到的是初始值 {}
。
如果你觉得 exports
与 module.exports
之间的区别很难分清,一个简单的处理方法,就是放弃使用 exports
,只使用 module.exports
。
*我个人也没觉得 exports
的写法有多方便,哈哈。
4. 总结
非常简单,就三点:
-
module.exports
初始值为一个空对象{}
; -
exports
是指向的module.exports
的引用; -
require()
返回的是module.exports
而不是exports
。
还是那句话,如果你觉得 exports
与 module.exports
之间的区别很难分清,一个简单的处理方法,就是放弃使用 exports
,只使用 module.exports
。
二、require() 扩展话题
以下案例源自知乎某帖回答,这里。
关于 require()
的解释
To illustrate the behavior, imagine this hypothetical implementation of require(), which is quite similar to what is actually done by require():
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// Module code here. In this example, define a function.
function someFunc() {}
exports = someFunc;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
module.exports = someFunc;
// At this point, the module will now export someFunc, instead of the
// default object.
})(module, module.exports);
return module.exports;
}
注意实现顺序,也就是下面代码为什么不成功的原因。
// moduleA.js
module.exports = function() {};
// 为什么这段配置不成功?你们有 BUG!!!
exports.abc = 'abc';
require()
的时候,是先通过 exports.abc
获取, 然后通过 module.exports
直接覆盖了原有的 exports
,所以 exports.abc = 'abc'
就无效了。
一般库的封装都是 exports = module.exports = _
(underscore 的例子)。
原因很简单,通过 exports = module.exports
让 exports
重新指向 module.exports
。
三、References
The end.