Node.js 的模块机制

Node.js 的模块机制

Posted by nolan on May 20, 2022

一、模块循环引用

问题一

假设有 a.js、b.js 两个模块相互引用,会有什么问题?是否为陷入死循环?看以下例子

// a.js
console.log('a模块start');

exports.test = 1;

undeclaredVariable = 'a模块未声明变量'

const b = require('./b');

console.log('a模块加载完毕: b.test值:',b.test);
// b.js
console.log('b模块start');

exports.test = 2;

const a = require('./a');

console.log('undeclaredVariable: ', undeclaredVariable);

console.log('b模块加载完毕: a.test值:', a.test);

解答:

启动 a.js 的时候,会加载 b.js,那么在 b.js 中又加载了 a.js,但是此时 a.js 模块还没有执行完,返回的是一个 a.js 模块的 exports 对象 未完成的副本 给到 b.js 模块(因此是不会陷入死循环的)。然后 b.js 完成加载之后将 exports 对象提供给了 a.js 模块

问题二

a 模块中的 undeclaredVariable 变量在 b.js 中是否会被打印?

控制台执行 node a.js,查看输出结果:

a模块start
b模块start
undeclaredVariable:  a模块未声明变量
b模块加载完毕: a.test值: 1
a模块加载完毕: b.test值: 2

解答:

因为 undeclaredVariable 是一个未声明的变量,也就是一个挂在全局的变量,那么在其他地方当然是可以拿到的。

在执行代码之前,Node.js 会使用一个代码封装器进行封装,例如下面所示:

(function(exports, require, module, __filename, __dirname) {
// 模块的代码
});

二、对象引用关系

也许是面试考察最多的问题:module.exports 与 exports 的区别?

问题一: module.exports 与 exports 的区别

exports 相当于 module.exports 的快捷方式如下所示:

const exports = modules.exports;

但是要注意不能改变 exports 的指向,我们可以通过 exports.test = ‘a’ 这样来导出一个对象, 但是不能向下面示例直接赋值,这样会改变 exports 的指向

// 错误的写法 将会得到 undefined
exports = {
  'a': 1,
  'b': 2
}

// 正确的写法
modules.exports = {
  'a': 1,
  'b': 2
}

三、 模块缓存

上面讲解了模块的加载机制,中间有提到模块初次加载之后会缓存起来,有没有疑问,模块缓存在哪里?

Node.js 提供了 require.cache API 查看已缓存的模块,返回值为对象,为了验证,这里做一个简单的测试,如下所示:

这里我导出一个变量和一个方法

module.exports = {
    a: 1,
    test: () => {}
}

新建 test-module.js 文件

require('./test-module.js');

console.log(require.cache);

在这个文件里加载 test-module.js 文件,在之后打印下 require.cache 看下里面返回的是什么?看到以下结果应该就很清晰了,模块的文件名、地址、导出数据都很清楚。

image