CommonJS规范与实现深度解析
CommonJS规范与实现深度解析
一句话概括
CommonJS 是 Node.js 采用的模块化规范,通过 require 同步加载模块、module.exports 导出接口,构建了服务器端的模块生态。
背景
在 ES Module 出现之前,JavaScript 没有官方模块系统。CommonJS 规范应运而生,最初为服务器端 JavaScript(Node.js)设计,后来也成为浏览器端打包工具(如 Webpack、Browserify)的模块标准。
为什么需要模块化?
- 避免全局变量污染
- 明确依赖关系
- 支持代码复用与拆分
- 便于维护和测试
概念与定义
核心概念
- 模块(Module):一个独立的 JavaScript 文件,拥有自己的作用域
- require():同步加载模块的函
- module.exports:模块导出对象
- exports:指向 module.exports 的引用(快捷方式)
模块作用域
每个 CommonJS 模块都有独立作用域,模块内部的变量、函数不会污染全局:
1
2
3
4
5
6
7
8
9
10
// module.js
const privateVar = '我只在本模块可见';
function privateFunc() {
return '私有方法';
}
module.exports = {
publicMethod: () => '公开方法'
};
最小示例
导出模块(math.js):
1
2
3
4
5
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = { add, subtract };
导入模块(main.js):
1
2
3
4
5
// main.js
const { add, subtract } = require('./math');
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
核心知识点拆解
1. require() 的查找规则
require() 接收模块标识符,按照以下顺序查找:
(1)核心模块(Node.js 内置)
1
2
3
const fs = require('fs'); // 文件系统模块
const path = require('path'); // 路径处理模块
const http = require('http'); // HTTP 模块
(2)相对路径 / 绝对路径
1
2
3
require('./module'); // 当前目录
require('../module'); // 上级目录
require('/module'); // 绝对路径
(3)node_modules 目录
1
require('lodash'); // 从 node_modules 查找
查找顺序:
- 当前目录的 node_modules
- 上级目录的 node_modules
- 逐级向上,直到根目录
(4)文件后缀自动补全
require('./module') 会依次尝试:
module.jsmodule.jsonmodule.node(C++ 插件)module/index.js(文件夹中的 index.js)
2. module.exports vs exports
常见误区:
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误用法:直接给 exports 赋值
exports = {
add: (a, b) => a + b
};
// 这样写无效!因为 exports 不再指向 module.exports
// ✅ 正确用法 1:给 module.exports 赋值
module.exports = {
add: (a, b) => a + b
};
// ✅ 正确用法 2:给 exports 添加属性
exports.add = (a, b) => a + b;
原理:
1
2
3
4
5
// Node.js 模块包装器
(function(exports, require, module, __filename, __dirname) {
// 你的模块代码在这里
exports.add = (a, b) => a + b;
});
exports只是module.exports的引用- 直接给
exports赋值会切断引用关系 - 最终导出的是
module.exports
3. 模块加载缓存机制
模块只会加载一次,后续调用返回缓存:
1
2
3
4
5
6
7
8
9
// counter.js
let count = 0;
module.exports = {
increment() {
count++;
return count;
}
};
1
2
3
4
5
6
// main.js
const counter1 = require('./counter');
const counter2 = require('./counter');
console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 2(共享同一个实例)
缓存机制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Node.js 内部实现(简化版)
const Module = {
_cache: {},
_load(filePath) {
// 1. 检查缓存
if (Module._cache[filePath]) {
return Module._cache[filePath].exports;
}
// 2. 创建模块实例
const module = new Module(filePath);
// 3. 加载并执行模块
module.load();
// 4. 缓存模块
Module._cache[filePath] = module;
// 5. 返回 exports
return module.exports;
}
};
4. 循环依赖的处理
CommonJS 能处理循环依赖,但可能返回不完整模块:
1
2
3
4
5
6
7
8
9
// a.js
console.log('a 开始加载');
exports.x = 'a1';
const b = require('./b');
console.log('在 a 中,b.y =', b.y);
exports.z = 'a2';
console.log('a 加载完成');
1
2
3
4
5
6
7
8
9
10
// b.js
console.log('b 开始加载');
exports.y = 'b1';
const a = require('./a');
console.log('在 b 中,a.x =', a.x);
console.log('在 b 中,a.z =', a.z); // undefined!
exports.w = 'b2';
console.log('b 加载完成');
执行结果:
1
2
3
4
5
6
7
a 开始加载
b 开始加载
在 b 中,a.x = a1
在 b 中,a.z = undefined
b 加载完成
在 a 中,b.y = b1
a 加载完成
原因:
- a.js 加载时,执行到
require('./b')暂停 - b.js 加载时,执行到
require('./a')直接从缓存获取 a.js 的module.exports - 此时 a.js 还没执行完,
exports.z还未定义
解决方案:
- 使用函数导出(运行时取值)
- 避免紧密耦合的循环依赖
- 使用 ES Module(静态分析更好)
实战案例
案例 1:创建一个工具库
项目结构:
1
2
3
4
5
6
7
my-utils/
├── package.json
├── lib/
│ ├── string.js
│ ├── array.js
│ └── object.js
└── index.js
lib/string.js:
1
2
3
4
5
6
7
8
9
module.exports = {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
},
camelCase(str) {
return str.replace(/-(\w)/g, (_, c) => c.toUpperCase());
}
};
lib/array.js:
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
unique(arr) {
return [...new Set(arr)];
},
flatten(arr) {
return arr.reduce((flat, item) =>
flat.concat(Array.isArray(item) ? this.flatten(item) : item), []
);
}
};
index.js(统一导出):
1
2
3
4
5
module.exports = {
...require('./lib/string'),
...require('./lib/array'),
...require('./lib/object')
};
使用:
1
2
3
4
const utils = require('my-utils');
console.log(utils.capitalize('hello')); // Hello
console.log(utils.unique([1, 2, 2, 3])); // [1, 2, 3]
案例 2:模拟 require() 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const Module = require('module');
const path = require('path');
const fs = require('fs');
function myRequire(filePath) {
// 1. 解析绝对路径
const absolutePath = path.resolve(filePath);
// 2. 检查缓存
if (Module._cache[absolutePath]) {
return Module._cache[absolutePath].exports;
}
// 3. 读取文件内容
const code = fs.readFileSync(absolutePath, 'utf-8');
// 4. 创建模块实例
const module = {
exports: {},
filename: absolutePath
};
// 5. 包装代码(模拟 Node.js 模块包装器)
const wrappedCode = `(function(exports, require, module, __filename, __dirname) {
${code}
})`;
// 6. 执行代码
const fn = eval(wrappedCode);
fn(module.exports, myRequire, module, absolutePath, path.dirname(absolutePath));
// 7. 缓存模块
Module._cache[absolutePath] = module;
// 8. 返回 exports
return module.exports;
}
底层原理
Node.js 模块包装器
Node.js 在执行模块代码前,会将其包装在一个函数中:
1
2
3
(function(exports, require, module, __filename, __dirname) {
// 模块代码
});
作用:
- 提供模块作用域(避免全局污染)
- 注入模块专有变量(
__filename,__dirname) - 提供导出接口(
exports,module)
模块加载流程(简化版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Node.js 内部实现(极度简化)
function loadModule(filename, module, require) {
const wrappedSrc = `(function(module, exports, require) {
${fs.readFileSync(filename, 'utf-8')}
})(module, module.exports, require);`;
eval(wrappedSrc);
}
function require(filename) {
const module = { exports: {} };
loadModule(filename, module, require);
return module.exports;
}
缓存机制的本质
1
2
3
4
5
6
7
8
9
// Module._cache 是一个对象
Module._cache = {
'/path/to/module.js': Module { /* 模块实例 */ }
};
// require() 时先查缓存
if (Module._cache[filename]) {
return Module._cache[filename].exports;
}
高频面试题解析
Q1: module.exports 和 exports 的区别是什么?
答:
exports是module.exports的引用(快捷方式)module.exports才是真正的导出对象- 直接给
exports赋值会切断引用关系,导致导出失败
1
2
3
4
5
6
7
8
// ✅ 正确:添加属性
exports.foo = 'bar';
// ✅ 正确:直接赋值给 module.exports
module.exports = { foo: 'bar' };
// ❌ 错误:直接给 exports 赋值
exports = { foo: 'bar' }; // 无效!
Q2: CommonJS 和 ES Module 的核心区别?
答:
| 特性 | CommonJS | ES Module |
|---|---|---|
| 加载方式 | 同步加载 | 异步加载 |
| 导入导出 | require / module.exports | import / export |
| 静态分析 | 不支持(运行时加载) | 支持(编译时解析) |
| 值拷贝 | 输出值的拷贝(缓存) | 输出值的引用(实时绑定) |
| 循环依赖 | 可能返回不完整模块 | 支持更好的循环依赖处理 |
| 使用场景 | Node.js 服务端 | 浏览器 + 服务端(通用) |
值拷贝 vs 值引用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CommonJS(值拷贝)
// counter.js
let count = 0;
module.exports = { count, increment };
function increment() {
count++;
}
// main.js
const { count, increment } = require('./counter');
console.log(count); // 0
increment();
console.log(count); // 还是 0!(count 是值拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
// ES Module(值引用)
// counter.mjs
export let count = 0;
export function increment() {
count++;
}
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1!(count 是实时绑定)
Q3: require() 的查找顺序是什么?
答:
- 缓存检查(
Module._cache) - 核心模块(Node.js 内置)
- 相对/绝对路径模块
node_modules目录(逐级向上查找)- 文件后缀自动补全(
.js→.json→.node→index.js)
Q4: CommonJS 如何处理循环依赖?有什么问题?
答:
- CommonJS 能处理循环依赖,但可能返回不完整的模块
- 原因:模块未执行完时就被缓存,后续加载返回不完整的
exports
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// a.js
exports.x = 'a1';
const b = require('./b');
exports.z = 'a2';
// b.js
exports.y = 'b1';
const a = require('./a');
console.log(a.x); // 'a1'(已定义)
console.log(a.z); // undefined(还未定义)
// main.js
const a = require('./a');
console.log(a.z); // 'a2'(完整导出)
解决方案:
- 避免循环依赖(重构代码)
- 导出函数(运行时取值)
- 使用 ES Module(静态分析更好)
总结与扩展
核心要点
- CommonJS 是 Node.js 的模块化规范
- 使用
require()同步加载 - 使用
module.exports导出
- 使用
- module.exports vs exports
exports是module.exports的引用- 直接给
exports赋值无效
- 模块缓存机制
- 模块只会加载一次
- 后续调用返回缓存的
exports
- 循环依赖的处理
- 可能返回不完整的模块
- 应避免紧密耦合的循环依赖
扩展学习
- ES Module 规范:
import/export语法 - 模块打包原理:Webpack、Rollup 的模块打包机制
- 动态导入:
require()的动态加载 vsimport()动态导入 - Tree Shaking:基于 ES Module 静态分析的死代码消除
实战建议
- 优先使用
module.exports- 避免
exports赋值的误区
- 避免
- 避免循环依赖
- 重构代码,解耦模块关系
- 合理使用缓存
- 利用模块缓存避免重复计算
- 迁移到 ES Module
- 新项目优先使用 ES Module
- 老项目逐步迁移(Node.js 支持 ES Module)
参考资料:
- Node.js 官方文档 - Modules
- 《深入浅出 Node.js》 - 朴灵
- CommonJS 规范 - commonjs.org
本文由作者按照 CC BY 4.0 进行授权