面试题答案
一键面试CommonJS模块系统的模块加载过程
- 路径解析:当使用
require
引入模块时,Node.js首先会根据传入的参数解析模块路径。如果是核心模块(如fs
、http
等),直接加载核心模块。如果是相对路径(以./
或../
开头)或绝对路径,会按照文件系统路径查找模块文件。如果是第三方模块,会在node_modules
目录中查找。 - 文件定位:找到模块文件后,根据文件扩展名确定加载方式。
.js
文件会被解析为JavaScript代码,.json
文件会被解析为JSON数据,.node
文件会被作为C++插件加载。如果没有扩展名,Node.js会依次尝试添加.js
、.json
、.node
扩展名进行查找。 - 模块编译:对于JavaScript模块,会将模块代码包装在一个函数中,这个函数的参数为
exports
、require
、module
、__filename
、__dirname
。通过这种方式,每个模块都有自己独立的作用域,避免变量污染。然后执行这个包装后的函数,模块的导出内容会被添加到exports
对象上。
循环依赖处理
在循环依赖场景下,Node.js会先加载被依赖模块的部分内容,然后返回一个不完整的模块对象。当被依赖模块执行完毕后,该模块对象才会完整。
模块缓存工作原理
Node.js使用一个全局的缓存对象来存储已经加载过的模块。当使用require
加载模块时,首先会检查缓存中是否已经存在该模块。如果存在,直接返回缓存中的模块导出对象,而不会再次加载和执行模块代码。这样可以提高模块加载效率,避免重复加载。
代码示例说明循环依赖场景下的表现和解决方法
假设有两个模块a.js
和b.js
,它们相互依赖。
a.js
console.log('a.js starting');
const b = require('./b');
console.log('a.js requiring b');
exports.message = 'Hello from a';
console.log('a.js finished');
b.js
console.log('b.js starting');
const a = require('./a');
console.log('b.js requiring a');
exports.message = 'Hello from b';
console.log('b.js finished');
main.js
console.log('main.js starting');
const a = require('./a');
console.log('main.js requiring a');
const b = require('./b');
console.log('main.js requiring b');
console.log(a.message);
console.log(b.message);
console.log('main.js finished');
表现:
- 当运行
main.js
时,a.js
开始加载,然后a.js
尝试加载b.js
。 b.js
开始加载,b.js
又尝试加载a.js
。由于a.js
正在加载中,Node.js会返回一个不完整的a
模块对象给b.js
。b.js
继续执行,完成加载并导出。a.js
继续执行,完成加载并导出。main.js
继续执行,获取到完整的a
和b
模块对象并输出信息。
解决方法: 尽量避免循环依赖。如果无法避免,可以将相互依赖的部分提取到一个独立的模块中,减少模块之间的直接循环引用。例如:
shared.js
exports.commonData = 'This is common data';
a.js
console.log('a.js starting');
const shared = require('./shared');
const b = require('./b');
console.log('a.js requiring b');
exports.message = 'Hello from a';
console.log('a.js finished');
b.js
console.log('b.js starting');
const shared = require('./shared');
const a = require('./a');
console.log('b.js requiring a');
exports.message = 'Hello from b';
console.log('b.js finished');
这样通过shared.js
模块,减少了a.js
和b.js
之间的直接循环依赖。