Appearance
CommonJS 模块系统面试题
1. CommonJS 模块规范
问题:什么是 CommonJS?它有哪些核心概念?
答案: CommonJS 是一种模块规范,最初为服务器端 JavaScript 设计,Node.js 采用了这一规范。
核心概念:
- 模块封装:每个文件都是一个独立的模块
- 模块导出:使用
module.exports或exports导出内容 - 模块导入:使用
require()函数导入模块 - 模块缓存:模块在第一次加载后被缓存,后续加载使用缓存
2. module.exports 和 exports 的区别
问题:module.exports 和 exports 有什么区别?
答案:
关系:
exports是module.exports的引用- 默认情况下,
exports === module.exports
区别:
module.exports是真正的导出对象exports只是module.exports的快捷方式
正确用法:
javascript
// 方式1:直接赋值给 module.exports
module.exports = {
foo: 'bar',
sayHello: function() {
console.log('Hello');
}
};
// 方式2:使用 exports(只能添加属性,不能重新赋值)
exports.foo = 'bar';
exports.sayHello = function() {
console.log('Hello');
};
// 错误用法:重新赋值 exports 会破坏引用关系
exports = { foo: 'bar' }; // 错误!不会导出3. require 的加载机制
问题:require() 是如何加载模块的?
答案:
加载流程:
- 路径分析:解析模块路径
- 文件定位:确定具体的文件位置
- 编译执行:编译并执行模块代码
- 缓存存储:将模块缓存起来
路径解析规则:
javascript
// 核心模块
const fs = require('fs');
// 相对路径模块
const utils = require('./utils');
const config = require('../config');
// 绝对路径模块
const absolute = require('/home/user/project/module');
// 第三方模块(从 node_modules 查找)
const express = require('express');文件查找顺序:
- 如果有确切文件名,直接加载
- 尝试添加扩展名:
.js→.json→.node - 如果是目录,查找
package.json的main字段 - 如果没有
package.json或main,查找index.js
4. 模块缓存机制
问题:Node.js 的模块缓存是如何工作的?
答案:
缓存机制:
- 模块在第一次加载后会被缓存
- 缓存的键是模块的绝对路径
- 后续
require()返回缓存的模块
查看缓存:
javascript
console.log(require.cache);
// 删除缓存
delete require.cache[require.resolve('./module')];缓存的影响:
javascript
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count
};
// main.js
const counter1 = require('./counter');
const counter2 = require('./counter'); // 返回同一个对象
console.log(counter1 === counter2); // true
counter1.increment();
console.log(counter2.getCount()); // 1(共享状态)5. 循环依赖问题
问题:什么是循环依赖?Node.js 如何处理循环依赖?
答案:
循环依赖: 当模块 A 依赖模块 B,模块 B 又依赖模块 A 时,形成循环依赖。
处理方式: Node.js 会返回已加载模块的未完成副本(partial exports)。
示例:
javascript
// a.js
console.log('a.js 开始加载');
const b = require('./b');
console.log('a.js 中 b.done =', b.done);
exports.done = true;
console.log('a.js 加载完成');
// b.js
console.log('b.js 开始加载');
const a = require('./a');
console.log('b.js 中 a.done =', a.done);
exports.done = true;
console.log('b.js 加载完成');
// main.js
console.log('main.js 开始加载');
const a = require('./a');
const b = require('./b');
console.log('main.js 中 a.done =', a.done, ', b.done =', b.done);输出:
main.js 开始加载
a.js 开始加载
b.js 开始加载
b.js 中 a.done = undefined
b.js 加载完成
a.js 中 b.done = true
a.js 加载完成
main.js 中 a.done = true , b.done = true避免循环依赖:
- 重构代码,提取公共部分
- 延迟加载(在函数内部 require)
- 使用事件驱动解耦
6. require 的同步加载
问题:为什么说 CommonJS 是同步加载的?这有什么影响?
答案:
同步加载:
require()是同步执行的- 会阻塞后续代码直到模块加载完成
- 适合服务器端,文件都在本地
影响:
javascript
// 同步加载,按顺序执行
const fs = require('fs');
const path = require('path');
const utils = require('./utils');
// 适合服务器端,不适合浏览器端与 ES Module 的对比:
- CommonJS:运行时同步加载
- ES Module:编译时静态分析,异步加载
7. 模块的加载优先级
问题:当存在同名模块时,Node.js 的加载优先级是什么?
答案:
加载优先级(从高到低):
- 核心模块(如
fs、http、path) - 相对路径文件模块(以
./或../开头) - 绝对路径文件模块
- 目录作为模块
- node_modules 中的第三方模块
示例:
javascript
// 优先加载核心模块
const http = require('http'); // 加载核心模块,而不是同名的 npm 包
// 相对路径优先于 node_modules
const utils = require('./utils'); // 加载当前目录的 utils.js
// 目录模块
const myPackage = require('./my-package'); // 加载目录中的 index.js 或 main 指定的文件8. 如何创建自定义模块
问题:如何创建和发布自定义的 Node.js 模块?
答案:
创建模块:
javascript
// my-module/index.js
class MyModule {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}!`;
}
}
module.exports = MyModule;package.json 配置:
json
{
"name": "my-custom-module",
"version": "1.0.0",
"main": "index.js",
"description": "A custom Node.js module",
"keywords": ["module", "custom"],
"author": "Your Name",
"license": "MIT"
}本地测试:
javascript
// 使用相对路径或 npm link
const MyModule = require('./my-module');
// 或
const MyModule = require('my-custom-module');发布到 npm:
bash
# 1. 登录 npm
npm login
# 2. 发布
npm publish
# 3. 更新版本
npm version patch # 或 minor、major
npm publish9. require 的性能优化
问题:如何优化 require() 的性能?
答案:
优化策略:
- 减少模块数量:只引入需要的模块
javascript
// 不推荐
const lodash = require('lodash');
const result = lodash.map(data, fn);
// 推荐
const map = require('lodash/map');
const result = map(data, fn);- 使用模块缓存:避免重复加载
javascript
// 模块只加载一次
const config = require('./config'); // 缓存- 延迟加载:按需加载模块
javascript
function heavyOperation() {
// 只在需要时加载
const heavyModule = require('./heavy-module');
return heavyModule.process();
}- 预加载关键模块:在应用启动时加载
javascript
// app.js
require('./config');
require('./database');
require('./cache');
// 后续使用时会命中缓存- 使用 ESM 的静态分析:迁移到 ES Module
javascript
// ESM 支持 Tree Shaking 和静态优化
import { map } from 'lodash-es';10. 热更新(Hot Module Replacement)
问题:如何在 Node.js 中实现模块的热更新?
答案:
实现原理: 清除模块缓存,重新加载模块。
简单实现:
javascript
function requireUncached(module) {
delete require.cache[require.resolve(module)];
return require(module);
}
// 使用
setInterval(() => {
const config = requireUncached('./config');
console.log('重新加载配置:', config);
}, 5000);使用 nodemon:
bash
# 开发时自动重启
npm install -g nodemon
nodemon app.js使用 pm2:
bash
# 生产环境热重载
pm2 start app.js --watch文件监听实现:
javascript
const fs = require('fs');
const path = require('path');
function watchModule(modulePath, callback) {
const fullPath = require.resolve(modulePath);
fs.watchFile(fullPath, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
delete require.cache[fullPath];
callback();
}
});
}
// 使用
watchModule('./config', () => {
console.log('配置文件已更新');
});