告别模块化焦虑:一篇读懂 CommonJS 和 ES6 Modules
场景推荐方案原因Node.js 原生开发CommonJS默认支持,无需配置。Node.js 配合 Babel/TypeScriptES6 模块可以使用更现代的语法,并享受 Tree Shaking 等优化,最终会被转译为 CommonJS 在 Node 中运行。前端工程化(Vue/React 项目)ES6 模块现代前端打包工具(Webpack, Vite, Rollup)都基于 ES6 模块进行静
居然还有人没搞明白CommonJS 和 ES6 模块到底有什么区别?**来来来,敲黑板一会要考~。我们来详细对比一下 JavaScript 中两种最重要的模块化规范:CommonJS 和 ES6 模块(ES Modules, ESM),带你起飞~ 芜湖。
首先模块化是什么?
简单来说,模块化就是把一个复杂的程序按照功能拆分成多个独立的文件。这样做的好处是:避免变量污染(不同文件间的变量不会互相干扰)、方便代码复用(哪里需要就在哪里引入)、易于维护(每个文件只负责一件事)。
CommonJS 和 ES6 模块就是两套不同的“拆文件和组合文件”的规则。
一、核心区别速览
在深入了解之前,可以先看一个直观的对比,有个整体印象:
| 特性 | CommonJS | ES6 模块 (ES Modules) |
|---|---|---|
| 设计初衷 | 为服务器端(如 Node.js)设计 | 为浏览器和服务器设计的官方标准 |
| 加载方式 | 动态(运行时加载) | 静态(编译时加载) |
| 加载时机 | 运行时(代码执行到require才加载) |
编译时(代码解析阶段就确定依赖关系) |
| 语法 | require() / module.exports |
import / export |
| 输出值 | 值的拷贝(基本类型是拷贝,对象是浅拷贝) | 值的引用(动态映射,只读但能反映模块内部变化) |
| 异步/同步 | 同步(适合本地文件读取) | 异步(天生支持,适合网络请求) |
| 使用环境 | Node.js 默认格式 | 现代浏览器、Node.js(需配置或使用.mjs) |
| 静态分析 | 不支持(无法在编译时检测错误) | 支持(可以进行 Tree Shaking) |
二、深入理解 CommonJS
1. 设计哲学
CommonJS 是 Node.js 默认的模块系统。它的设计目标是让 JavaScript 能在服务器端运行。因为模块文件都存在本地硬盘上,读取速度非常快,所以设计成了同步加载。
2. 语法与用法
-
导出:使用
module.exports或exports对象。// 📁 math.js const add = (a, b) => a + b; const subtract = (a, b) => a - b; // 方式一:导出整个对象 module.exports = { add, subtract }; // 方式二:也可以直接给 exports 添加属性(但注意不能直接覆盖 exports 对象) // exports.add = add; // exports.subtract = subtract; -
导入:使用
require()函数。// 📁 main.js const math = require('./math.js'); console.log(math.add(2, 3)); // 输出 5
3. 关键特性:运行时加载与值的拷贝
-
运行时加载:
require是在代码执行到这一行时,才去读取文件、执行代码、获取module.exports的值。因此,require语句可以写在条件语句里(比如if条件满足时才加载模块)。 -
值的拷贝:对于基本类型(Number, String, Boolean),导入的是这个值的副本。如果在原模块内部修改了这个值,导入的模块是看不到变化的。对于对象,因为是浅拷贝,导入的对象和原模块对象指向同一内存地址。
// 📁 counter.js let count = 0; const increment = () => count++; module.exports = { count, increment }; // 📁 app.js const { count, increment } = require('./counter.js'); console.log(count); // 输出 0 increment(); console.log(count); // 仍然输出 0 (因为 count 是基本类型的拷贝,不是引用)
三、深入理解 ES6 模块
1. 设计哲学
ES6 模块是 ECMAScript 官方标准。它被设计成静态的,目的是让 JavaScript 引擎在代码编译阶段就能确定模块的依赖关系,从而进行优化。
2. 语法与用法
-
导出:使用
export关键字。// 📁 math.js // 方式一:逐个导出(命名导出) export const add = (a, b) => a + b; export const subtract = (a, b) => a - b; // 方式二:默认导出(一个模块只能有一个默认导出) const multiply = (a, b) => a * b; export default multiply; -
导入:使用
import关键字。// 📁 main.js // 导入命名导出(名称必须和导出时一致) import { add, subtract } from './math.js'; // 导入默认导出(可以自定义名称) import multiply from './math.js'; // 或者一次性全部导入 import * as math from './math.js'; console.log(math.add(2, 3));
3. 关键特性:编译时加载与值的引用
-
编译时加载(静态执行):
import命令会被 JavaScript 引擎在编译阶段提升到模块顶部进行处理。它不能放在条件语句或函数里。这带来了一个巨大的好处:静态分析。工具可以遍历模块依赖树,打包时移除那些被导入但从未被使用的代码,这就是 Tree Shaking。 -
值的引用(动态绑定):
import导入的是对原始模块内部值的只读引用。这意味着,如果原模块内的值发生了变化,导入模块中读到的值也会随之变化。// 📁 counter.js export let count = 0; export const increment = () => count++; // 📁 app.js import { count, increment } from './counter.js'; console.log(count); // 输出 0 increment(); console.log(count); // 输出 1 (因为是引用,读到了最新的值)
四、总结与建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| Node.js 原生开发 | CommonJS | 默认支持,无需配置。 |
| Node.js 配合 Babel/TypeScript | ES6 模块 | 可以使用更现代的语法,并享受 Tree Shaking 等优化,最终会被转译为 CommonJS 在 Node 中运行。 |
| 前端工程化(Vue/React 项目) | ES6 模块 | 现代前端打包工具(Webpack, Vite, Rollup)都基于 ES6 模块进行静态分析和 Tree Shaking,打包出体积更小的代码。 |
| 现代浏览器(直接使用) | ES6 模块 | 在 <script type="module"> 中可以直接使用,浏览器会帮你处理依赖和异步加载。 |
简单来说,CommonJS 是 Node.js 的“老朋友”,稳定可靠;ES6 模块是整个 JavaScript 生态的未来,它带来的静态优化能力让代码更高效。如果你现在开始一个新的前端项目,ES6 模块无疑是首选。
更多推荐
所有评论(0)