toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

module.exports 属性与 exports 变量的区别 #218

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

一、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 变量指向一个值,因为这样等于切断了 exportsmodule.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 的指向,即切断了 exportsmodule.exports 的联系。但是我们模块对外输出的接口是 module.exports,所以 2️⃣ 得到的是初始值 {}

如果你觉得 exportsmodule.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用 exports,只使用 module.exports

*我个人也没觉得 exports 的写法有多方便,哈哈。

4. 总结

非常简单,就三点:

还是那句话,如果你觉得 exportsmodule.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.exportsexports 重新指向 module.exports

三、References

The end.