Skip to content

CommonJS 模块系统面试题

1. CommonJS 模块规范

问题:什么是 CommonJS?它有哪些核心概念?

答案: CommonJS 是一种模块规范,最初为服务器端 JavaScript 设计,Node.js 采用了这一规范。

核心概念

  • 模块封装:每个文件都是一个独立的模块
  • 模块导出:使用 module.exportsexports 导出内容
  • 模块导入:使用 require() 函数导入模块
  • 模块缓存:模块在第一次加载后被缓存,后续加载使用缓存

2. module.exports 和 exports 的区别

问题module.exportsexports 有什么区别?

答案

关系

  • exportsmodule.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() 是如何加载模块的?

答案

加载流程

  1. 路径分析:解析模块路径
  2. 文件定位:确定具体的文件位置
  3. 编译执行:编译并执行模块代码
  4. 缓存存储:将模块缓存起来

路径解析规则

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');

文件查找顺序

  1. 如果有确切文件名,直接加载
  2. 尝试添加扩展名:.js.json.node
  3. 如果是目录,查找 package.jsonmain 字段
  4. 如果没有 package.jsonmain,查找 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 的加载优先级是什么?

答案

加载优先级(从高到低):

  1. 核心模块(如 fshttppath
  2. 相对路径文件模块(以 ./../ 开头)
  3. 绝对路径文件模块
  4. 目录作为模块
  5. 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 publish

9. require 的性能优化

问题:如何优化 require() 的性能?

答案

优化策略

  1. 减少模块数量:只引入需要的模块
javascript
// 不推荐
const lodash = require('lodash');
const result = lodash.map(data, fn);

// 推荐
const map = require('lodash/map');
const result = map(data, fn);
  1. 使用模块缓存:避免重复加载
javascript
// 模块只加载一次
const config = require('./config'); // 缓存
  1. 延迟加载:按需加载模块
javascript
function heavyOperation() {
  // 只在需要时加载
  const heavyModule = require('./heavy-module');
  return heavyModule.process();
}
  1. 预加载关键模块:在应用启动时加载
javascript
// app.js
require('./config');
require('./database');
require('./cache');

// 后续使用时会命中缓存
  1. 使用 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('配置文件已更新');
});