第1章 1.3 模块系统 CommonJS
⚠️ 注意:你标题写的是「Python 教程」,但 CommonJS 是 Node.js 的模块系统,内容也全是
require/module.exports这些 JS 概念。我按 Node.js 来写,如果确实要 Python 版本告诉我再调整。
🎯 开场 3 分钟:为什么要学这个?
上一章我们学了两种运行 JS 的方式:交互式的 REPL 和脚本模式。你有没有发现一个问题——
当代码超过 300 行的时候,整个文件塞在一个 app.js 里,自己都看不下去?
更难受的是,如果你写了一个「计算双十一打折」的函数,想在另一个脚本里用,得:
- 把代码复制粘贴过去(改 bug 要改两处)
- 或者建一个 .html 文件通过 <script> 引入(浏览器才这么做)
Node.js 给你第三种方式:模块系统。
学完这一章,你能:
- 把大文件拆成多个小文件,想用哪个 require() 进来
- 写一个「工具箱」给别人用,你自己的代码也不污染别人的命名空间
- 搞懂为什么有时候改了代码但运行结果没变(缓存的坑)
🧱 基础 25 分钟:核心概念
1.3.1 什么是模块?
生活类比:你家的「工具箱」。
想象你有一个工具箱,里面有螺丝刀、锤子、扳手。你修电脑的时候抽出来用,修完再放回去。你不会担心「螺丝刀被别人借走了」,因为工具箱是独立的,你用完别人还能用。
在代码里:一个 .js 文件就是一个模块。你在这个文件里定义的变量、函数,默认只在文件内部生效,不会跑到外面去。
1.3.2 第一个模块:导出和导入
怎么用:
首先,创建一个文件叫 greet.js,内容如下:
// greet.js
function sayHello(name) {
return '你好,' + name + '!';
}
function sayGoodbye(name) {
return '再见,' + name + '!';
}
// 导出:把这两个函数「放工具箱里」
module.exports = {
sayHello: sayHello,
sayGoodbye: sayGoodbye
};
然后,创建 main.js,内容如下:
// main.js
// 导入:把工具箱拿过来用
const greet = require('./greet');
console.log(greet.sayHello('小明'));
console.log(greet.sayGoodbye('小明'));
运行 node main.js,输出:
你好,小明!
再见,小明!
这行在干嘛:
- module.exports = {...} = 把东西装进工具箱
- require('./greet') = 把工具箱拿过来
1.3.3 简写形式
如果属性名和变量名一样,可以偷懒:
// greet.js 简写版
function sayHello(name) {
return '你好,' + name + '!';
}
function sayGoodbye(name) {
return '再见,' + name + '!';
}
module.exports = {
sayHello,
sayGoodbye
};
效果完全一样,只是少写了几次名字。
1.3.4 只导出一个函数(常用简写)
有时候你只想导出「一个东西」,比如一个计算器函数:
// calc.js
function add(a, b) {
return a + b;
}
// 导出单个函数,直接等于这个函数
module.exports = add;
// main.js
const add = require('./calc');
console.log(add(1, 2)); // 3
这就是 CommonJS 的核心:require() 拿过来,module.exports 送出去。

1.3.5 内置模块:Node.js 给你准备好的工具箱
Node.js 自带很多内置模块,不用安装直接用:
// 路径处理模块
const path = require('path');
// 获取当前文件的所在目录
console.log(__dirname); // 当前文件所在文件夹的绝对路径
console.log(path.basename(__dirname)); // 文件夹名字
// 拼接路径(自动处理 / 和 \ 的问题)
const fullPath = path.join(__dirname, 'public', 'index.html');
console.log(fullPath);
运行后你会看到类似:
/Users/apple/workspace/myproject
myproject
/Users/apple/workspace/myproject/public/index.html
为什么要用 path.join:直接字符串拼接 __dirname + '/public/index.html' 在 Windows 上会变成 \ 和 / 混用,容易出 bug。
1.3.6 npm 下载的模块怎么用?
第三章会详细讲 npm,现在先知道这么用:
// 假设你执行了:npm install lodash
const _ = require('lodash');
const arr = [1, 2, 3, 4, 5];
console.log(_.chunk(arr, 2)); // [[1,2], [3,4], [5]]
直接 require('lodash') 就行,不用加 ./。

1.3.7 模块缓存:同一个模块只执行一次
这是新手容易踩的坑,看例子:
// counter.js
let count = 0;
count += 1;
console.log('counter.js 被加载了,第' + count + '次');
module.exports = { count };
// main.js
const a = require('./counter');
const b = require('./counter');
console.log('a.count =', a.count); // 1
console.log('b.count =', b.count); // 1
运行 node main.js,你猜输出什么?
counter.js 被加载了,第1次
a.count = 1
b.count = 1
注意:虽然 require 了两次,但 counter.js 只执行了一次!所以 count 只被加了一次。
这是因为 Node.js 会缓存每个加载过的模块。第二次 require 时,直接从缓存拿结果,不重新执行代码。
这意味着:如果你改了被引用模块的代码,需要重启 Node.js 才能看到效果。保存代码但没重启?结果没变?十有八九是缓存的锅。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):小明的购物计算器 🛒
场景:小明买苹果 3 斤,每斤 5 块;香蕉 2 斤,每斤 3 块。算总价和找零。
先建 price_calc.js:
// price_calc.js
function calcTotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.count;
}
return total;
}
function giveChange(money, total) {
return money - total;
}
module.exports = { calcTotal, giveChange };
再建 main.js:
// main.js
const { calcTotal, giveChange } = require('./price_calc');
const shoppingList = [
{ name: '苹果', price: 5, count: 3 },
{ name: '香蕉', price: 3, count: 2 }
];
const total = calcTotal(shoppingList);
const payment = 30;
const change = giveChange(payment, total);
console.log('商品列表:');
for (const item of shoppingList) {
console.log(` ${item.name} x ${item.count} = ${item.price * item.count}元`);
}
console.log('总计:' + total + '元');
console.log('付款:' + payment + '元');
console.log('找零:' + change + '元');
运行 node main.js:
商品列表:
苹果 x 3 = 15元
香蕉 x 2 = 6元
总计:21元
付款:30元
找零:9元
一句话解释:把计算逻辑单独放一个模块,主程序只负责调用,代码干净多了。
项目 2(15 分钟):读取 CSV 文件处理数据
场景:有一个 students.csv 文件,存着学生成绩,要算出平均分。
CSV 文件内容(students.csv):
name,chinese,math,english
张三,85,92,78
李四,90,88,95
王五,76,85,90
建 csv_parser.js:
// csv_parser.js
const fs = require('fs');
const path = require('path');
function readCSV(filename) {
const filePath = path.join(__dirname, filename);
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.trim().split('\n');
// 第一行是表头
const headers = lines[0].split(',');
// 剩下的行是数据
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',');
const row = {};
for (let j = 0; j < headers.length; j++) {
row[headers[j]] = values[j];
}
data.push(row);
}
return data;
}
function calcAverage(data, subject) {
let sum = 0;
for (const row of data) {
sum += parseFloat(row[subject]);
}
return (sum / data.length).toFixed(2);
}
module.exports = { readCSV, calcAverage };
建 main.js:
// main.js
const { readCSV, calcAverage } = require('./csv_parser');
const students = readCSV('students.csv');
console.log('学生成绩数据:');
console.log(students);
console.log('\n各科平均分:');
console.log('语文:' + calcAverage(students, 'chinese'));
console.log('数学:' + calcAverage(students, 'math'));
console.log('英语:' + calcAverage(students, 'english'));
运行 node main.js:
学生成绩数据:
[
{ name: '张三', chinese: '85', math: '92', english: '78' },
{ name: '李四', chinese: '90', math: '88', english: '95' },
{ name: '王五', chinese: '76', math: '85', english: '90' }
]
各科平均分:
语文:83.67
数学:88.33
英语:87.67
一句话解释:读取文件、解析数据、计算统计,三个功能拆成三个部分,逻辑清晰。
项目 3(15 分钟):命令行待办清单工具
场景:写一个命令行工具,可以添加任务、查看任务列表、标记完成。
建 todo_store.js(数据存储模块):
// todo_store.js
const fs = require('fs');
const path = require('path');
const DATA_FILE = path.join(__dirname, 'todos.json');
function loadTodos() {
if (!fs.existsSync(DATA_FILE)) {
return [];
}
const content = fs.readFileSync(DATA_FILE, 'utf-8');
return JSON.parse(content);
}
function saveTodos(todos) {
fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2));
}
function addTodo(text) {
const todos = loadTodos();
todos.push({
id: Date.now(),
text: text,
done: false
});
saveTodos(todos);
return todos;
}
function listTodos() {
return loadTodos();
}
function doneTodo(id) {
const todos = loadTodos();
for (const todo of todos) {
if (todo.id == id) {
todo.done = true;
}
}
saveTodos(todos);
return todos;
}
module.exports = { addTodo, listTodos, doneTodo };
建 main.js(入口程序):
// main.js
const { addTodo, listTodos, doneTodo } = require('./todo_store');
// 获取命令行参数(node main.js add "买牛奶" 这样的格式)
const action = process.argv[2];
const arg = process.argv[3];
if (action === 'add') {
if (!arg) {
console.log('请输入任务内容:node main.js add "任务名称"');
return;
}
addTodo(arg);
console.log('已添加任务:' + arg);
} else if (action === 'list') {
const todos = listTodos();
console.log('\n待办清单:');
todos.forEach((todo, index) => {
const status = todo.done ? '✅' : '⬜';
console.log(`${index + 1}. ${status} ${todo.text}`);
});
} else if (action === 'done') {
if (!arg) {
console.log('请输入任务ID:node main.js done 任务ID');
return;
}
doneTodo(arg);
console.log('已标记完成');
} else {
console.log('用法:');
console.log(' node main.js add "任务名称" - 添加任务');
console.log(' node main.js list - 查看列表');
console.log(' node main.js done 任务ID - 标记完成');
}
运行测试:
node main.js add "买牛奶"
node main.js add "写周报"
node main.js list
node main.js done 上面显示的ID
node main.js list
预期输出:
已添加任务:买牛奶
已添加任务:写周报
待办清单:
1. ⬜ 买牛奶
2. ⬜ 写周报
待办清单:
1. ✅ 买牛奶
2. ⬜ 写周报
一句话解释:数据存在 JSON 文件里,程序重启后数据还在,下次课你会学到更优雅的存储方式。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:循环依赖导致 undefined
❌ 错误示例:
// a.js
const { bMethod } = require('./b');
console.log('a.js 调用 b 的方法:', bMethod); // undefined!
module.exports = { aMethod: () => 'A方法' };
// b.js
const { aMethod } = require('./a');
console.log('b.js 调用 a 的方法:', aMethod); // undefined!
module.exports = { bMethod: () => 'B方法' };
// main.js
require('./a');
运行结果:
b.js 调用 a 的方法: undefined
a.js 调用 b 的方法: undefined
✅ 正确做法:避免循环依赖,或者把共享的代码提取到第三个文件。
// shared.js(独立的共享模块)
module.exports = { sharedData: 'hello' };
// a.js
const { sharedData } = require('./shared');
const { bMethod } = require('./b');
console.log('现在能拿到:', bMethod); // 能拿到
module.exports = { aMethod: () => 'A方法' };
坑 2:require 路径写错
❌ 错误示例:
const myModule = require('myModule'); // 找不到,报错
const myModule = require('./myModule'); // 缺扩展名,可能找不到
const myModule = require('../myModule'); // 目录不对
✅ 正确做法:
// 本地模块,带扩展名
const myModule = require('./myModule.js');
// node_modules 模块,不用路径
const express = require('express');
// 目录名(自动找 index.js)
const myLib = require('./myLib');
坑 3:修改了模块但效果没变
原因:Node.js 缓存了模块,改了代码没重启不会生效。
✅ 正确做法:
- 每次改代码后重新运行 node xxx.js
- 或者用 nodemon 自动监听重启(后续章节会讲)
坑 4:module.exports 和 exports 混用
❌ 错误示例:
// 错误!这样会切断联系
exports.sayHello = () => 'hi';
module.exports = { sayGoodbye: () => 'bye' };
// 上面两行,exports 那行白写了!
✅ 正确做法:二选一,不要混用。
// 方式一:只用 module.exports
module.exports = {
sayHello: () => 'hi'
};
// 方式二:只用 exports(但最后还是要赋值给 module.exports)
exports.sayHello = () => 'hi';
// 如果要导出整个对象,必须这样:
module.exports = exports;
坑 5:相对路径在不同目录结果不同
❌ 错误示例:
// 在 ./utils/helper.js 里
const config = require('../config.json'); // 相对于 helper.js 的位置
// 在根目录运行 node ./utils/helper.js,能找到
// 但如果 node main.js(main.js 里 require('./utils/helper')),路径就错了
✅ 正确做法:用 __dirname 拼接绝对路径:
const path = require('path');
const configPath = path.join(__dirname, '..', 'config.json');
const config = require(configPath);
调试技巧:console.log + 彩色输出
// 简单的调试方法
console.log('变量 a 的值是:', a);
// 彩色输出,更醒目
console.log('\x1b[31m错误:\x1b[0m 文件读取失败'); // 红色
console.log('\x1b[32m成功:\x1b[0m 操作完成'); // 绿色
console.log('\x1b[33m警告:\x1b[0m 库存不足'); // 黄色
✏️ 练习题
练习 1(2 分钟):改改购物车
题目:把项目 1 的苹果改成 5 斤,香蕉改成 3 斤,看看总价变成多少。
- 输入:修改
shoppingList数组 - 预期输出:新的总价
- 提示:直接改
count值就行
练习 2(2 分钟):加个判断
题目:在 price_calc.js 里加个函数 isEnoughMoney(wallet, total),如果 wallet >= total 返回 true,否则返回 false。
- 输入:
isEnoughMoney(30, 21) - 预期输出:
true - 提示:用
>=比较
练习 3(3 分钟):处理新 CSV
题目:新建一个 grades.csv,包含三行数据(姓名、分数),用项目 2 的方法算出平均分。
- 输入:自己创建的
grades.csv - 预期输出:平均分数字
- 提示:把
students.csv复制一份改改内容试试
练习 4(5 分钟):串起两个项目
题目:把项目 2 的 CSV 读取结果,传给项目 1 的计算器。
- 输入:CSV 里的语文、数学、英语分数
- 预期输出:各科平均分和总平均分
- 提示:把
calcAverage的结果传给calcTotal
练习 5(5 分钟):读报错信息
题目:运行下面代码会报错,修复它。
// test.js
const result = require('./nonexistent');
console.log(result);
- 预期输出:
Error: Cannot find module './nonexistent' - 提示:检查路径是否正确,文件是否存在
📚 作业:做一个命令行记账工具
需求描述:帮小明记零花钱,支持收入、支出、查余额。
功能点:
1. node main.js add 收入 100 "发工资" - 记一笔收入
2. node main.js spend 50 "买书" - 记一笔支出
3. node main.js balance - 查看余额和明细
加分项:
1. 数据存到 ledger.json 文件,重启程序后不丢失
2. 支出时如果余额不足,提示「余额不足,无法支出」
验收标准:
- 能跑起来
- 收支记录正确
- 余额计算正确
- 提交方式:评论区贴代码
📚 总结 + 资源
本文学了 3 件事:
1. require() 导入模块,module.exports 导出模块
2. 内置模块(fs、path)和第三方模块(lodash)的用法
3. 模块缓存机制和常见坑(循环依赖、路径错误)
延伸资源:
- Node.js 官方文档 - 模块(英文,但例子很清楚)
- 《Node.js 实战:用 JavaScript 构建高性能服务器》(中文经典书)
- 视频:B 站「Node.js 入门教程」系列
互动钩子:你在项目里有没有遇到过「改了代码但没效果」的坑?当时是怎么发现的?评论区聊聊,老粉优先回复!
下一章我们要解决一个问题:CommonJS 虽然好用,但它是 Node.js 专属的,浏览器不认识。ES Modules(import/export)才是未来的标准,而且写法更简洁。剧透一下:到时候「小明的购物清单」要换个方式登场了。

评论(0)