news 2026/9/28 6:41:45

一文说清ES6模块化:与CommonJS的核心差异解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文说清ES6模块化:与CommonJS的核心差异解析

从 CommonJS 到 ES6 模块:一次彻底的 JavaScript 模块化进化

你有没有遇到过这种情况?明明只用了一个轻量工具函数,打包后却发现整个库都被塞进了 bundle;或者在写 Node.js 服务时,想按需加载某个功能模块,却因为require的同步特性卡住了响应。

这些问题的背后,其实是 JavaScript 模块系统的代际差异——CommonJS 和 ES6 Module(ESM)的根本不同。它们不只是语法上的“importvsrequire”,更代表了两种截然不同的模块哲学:一个是运行时动态拼接,一个是编译期静态规划。

今天我们就来彻底讲清楚这场 JavaScript 模块化的关键跃迁:为什么 ESM 成为了现代开发的事实标准?它到底比 CommonJS 强在哪里?以及我们在实际项目中该如何正确使用。


为什么需要新的模块系统?

在 ES6 出现之前,JavaScript 语言本身没有原生模块机制。于是社区创造了各种方案,其中CommonJS因为简洁直观,在 Node.js 中被广泛采用:

// math.js function add(a, b) { return a + b; } module.exports = { add }; // app.js const { add } = require('./math'); console.log(add(2, 3));

这看起来没问题,尤其在服务器端,文件都在本地磁盘上,同步读取很快。但当这套模式被搬到浏览器环境时,问题就暴露出来了:

  • 阻塞加载:require是同步的,浏览器必须等一个模块下载并执行完才能继续;
  • 无法静态分析:构建工具不知道你什么时候、会加载哪个模块,没法提前优化;
  • Tree Shaking 失效:即使你只用了某个大模块里的一两个方法,整个文件也会被打包进去。

随着前端应用越来越复杂,性能和体积成了瓶颈。我们需要一种更高效、更适合现代工程化的模块方式。于是,ES6 模块应运而生。


ES6 模块的核心思想:静态优先

ES6 模块最大的突破不是多了import/export这两个关键字,而是它的静态性设计原则。

什么叫“静态”?意思是:

所有导入导出关系,在代码执行前就已经确定了。

这意味着什么?

✅ 构建工具可以“看懂”你的依赖

比如这段代码:

import { formatDate } from './utils/date.js'; import api from './api/client.js'; export default function render() { const now = formatDate(new Date()); return `<p>${now}</p>`; }

打包工具(如 Webpack、Vite、esbuild)在解析阶段就能提取出:
- 当前模块依赖./utils/date.js的formatDate
- 依赖./api/client.js的默认导出
- 自己对外暴露一个默认导出

不需要运行代码,就能画出完整的依赖图谱。这就为后续的各种优化打开了大门。

🚫 不允许动态表达式(顶层静态限制)

正因为要保证静态可分析,ES6 对import做了严格约束:

// ❌ 错误:不能用变量或条件判断 if (env === 'dev') { import devTool from './dev-tools.js'; // SyntaxError! } const name = 'logger'; import { name } from './tools.js'; // 不行!name 必须是字面量

不过别担心,ES6 提供了动态导入作为补充:

// ✅ 动态导入:返回 Promise async function loadAdminPanel() { if (user.isAdmin) { const { AdminDashboard } = await import('./admin.js'); render(AdminDashboard); } }

这样既保留了静态主干的可预测性,又通过import()实现了运行时灵活性。


关键差异一:绑定机制完全不同

这是最容易被忽略、却最致命的区别。

CommonJS:值拷贝(Value Copy)

我们来看一个经典陷阱:

// counter.js let count = 0; setTimeout(() => count++, 100); module.exports = { count };
// app.js const { count } = require('./counter'); console.log(count); // 输出 0 // 一秒后,原模块里的 count 已经变成 1,但这里还是 0

原因很简单:require返回的是一个对象快照。一旦导入完成,就跟原模块断开联系了。后续变化不会反映到导入方。

ES6 Module:动态绑定(Live Binding)

同样的逻辑换成 ESM:

// counter.mjs export let count = 0; export const increment = () => { count++; }; setTimeout(() => increment(), 100);
// main.mjs import { count, increment } from './counter.mjs'; console.log(count); // 0 setTimeout(() => console.log(count), 200); // 1!能感知到变化

看到区别了吗?ES6 模块中的导入不是拷贝,而是一个实时引用。只要你访问count,拿到的就是当前最新的值。

这种机制让模块间的通信更加灵活,也避免了很多因“状态不同步”引发的 bug。


关键差异二:加载时机与执行模型

维度CommonJSES6 Module
加载时机运行时动态加载编译时静态解析
执行顺序立即执行分离解析、实例化、执行三阶段
是否缓存是(require第二次直接返回缓存)是(单例共享)

举个例子说明执行流程的不同。

CommonJS:边加载边执行
// a.js console.log('a starting'); const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); const a = require('./a'); // 循环引用! console.log('in b, a.done =', a.done); exports.done = true; console.log('b done');

输出结果:

a starting b starting in b, a.done = undefined b done in a, b.done = true a done

可以看到,当b.js中require('./a')时,a.js还没执行完,所以a.done是undefined——部分初始化状态暴露了出来,容易导致意外行为。

ES6 Module:延迟绑定,安全处理循环引用
// x.mjs console.log('x start'); import { y } from './y.mjs'; export const x = 'from x'; console.log('x.y =', y); // y.mjs console.log('y start'); import { x } from './x.mjs'; export const y = 'from y'; console.log('y.x =', x);

输出:

x start y start y.x = undefined x.y = from y

虽然也有循环引用,但由于 ESM 使用的是动态绑定 + 提前声明机制,即使x尚未赋值,也不会报错,而是表现为undefined。等到真正访问时,如果已经初始化,则能拿到最新值。

这使得 ESM 在面对复杂依赖网络时更加健壮。


真正的价值:推动前端工程化跃迁

ES6 模块的意义远不止语法更新,它直接催生了一系列现代前端核心技术:

1. Tree Shaking:删除无用代码

由于 ESM 是静态结构,构建工具可以精确追踪哪些导出从未被引用,从而安全移除。

// utils.js export function fastSort(arr) { /* ... */ } export function slowSort(arr) { /* ... */ } // main.js import { fastSort } from './utils'; fastSort([3,1,4]);

在这种情况下,slowSort完全不会进入最终打包结果。而如果是 CommonJS:

// common-utils.js exports.fastSort = function() { /* ... */ }; exports.slowSort = function() { /* ... */ }; // app.js const { fastSort } = require('./common-utils');

打包工具无法确定slowSort是否会被其他地方调用(毕竟require可以出现在任何位置),只能保守地全部保留。

2. Code Splitting:按需加载

结合动态import(),我们可以轻松实现懒加载:

router.on('/settings', async () => { const { SettingsPage } = await import('./pages/Settings.js'); render(<SettingsPage />); });

Webpack/Vite 会自动将Settings.js及其依赖拆分为独立 chunk,只在路由命中时才加载,显著提升首屏性能。

3. Scope Hoisting:减少闭包开销

Rollup 和 Vite 支持将多个模块合并到同一个作用域中,避免每个模块都包装成单独函数闭包,减少内存占用和执行开销。

这一切的前提,都是 ESM 的静态可分析性。


实战建议:如何正确使用 ESM?

✔️ 推荐做法

  1. 新项目一律使用 ESM
    - 前端不用说,Node.js 新项目也推荐启用"type": "module"或使用.mjs扩展名;
    - 可以使用 top-level await:
    js const config = await fetch('/config.json').then(r => r.json()); export { config };

  2. 统一导出风格
    - 要么全用命名导出:
    js export const API_URL = '...'; export function request() { }
    - 要么明确区分主功能用默认导出:
    js export default class Router { } export function parsePath() { }

  3. 合理聚合导出(re-export)

创建公共入口文件,方便使用者一次性导入:

js // index.js export { useAuth } from './hooks/useAuth.js'; export * from './components/Button.js'; export { apiClient } from './services/api.js';

使用者只需:
js import { useAuth, Button, apiClient } from '@lib/ui';

  1. 注意路径扩展名
    - 浏览器和 Deno 要求显式写出.js;
    - Vite 默认要求,Node.js ESM 模式也需要;
    - 别再写import '@/utils',应该写import '@/utils/index.js'

  2. 跨平台兼容处理

在 ESM 中获取当前文件路径:

js // ❌ __dirname 不可用 // ✅ 使用 import.meta.url const currentDir = new URL('.', import.meta.url).pathname;


写在最后:不只是语法升级

很多人把import/export当作简单的语法糖,其实不然。

CommonJS 是“运行时模块”:像搭积木一样,一边执行一边拼装。
ES6 Module 是“编译时模块”:先画蓝图,再施工,全局可控。

正是这种转变,让我们能够实现:
- 更小的包体积(Tree Shaking)
- 更快的加载速度(Code Splitting)
- 更强的类型支持(静态分析)
- 更可靠的依赖管理(循环引用保护)

如今,无论是 React/Vue 的组件系统,还是 Vite/esbuild 的极速构建,背后都建立在 ESM 的静态基石之上。就连新兴运行时 Bun、Deno,也都原生只支持 ESM。

可以说,掌握 ES6 模块化,已经不再是“会不会用import”的问题,而是能否理解现代 JavaScript 工程体系运转逻辑的关键所在。

下次当你敲下import { createApp } from 'vue'的时候,不妨想一想:这个简单的语句背后,是一整套从静态解析到动态绑定、从依赖收集到代码分割的技术链条在支撑着它。

而这,正是现代前端的魅力所在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 11:51:41

文心Moment·上海站|Agent训练营:把Agent玩明白,侬来伐

听说文心开年有大动作&#xff1f; 听说可以体验文心最新模型&#xff1f; 听说Agent训练营第三期来了&#xff1f; 听说是上海&#xff1f; 请注意&#xff0c;大事加载中…… 百度智能云的AI硬件实践&#xff1a;一块模组里的“工匠对话” 点击“阅读原文”&#xff0c;立即合…

作者头像 李华
网站建设 2026/9/27 8:43:46

构建高效XDMA数据通路的关键步骤:操作指南

如何让FPGA“飞”起来&#xff1f;XDMA数据通路实战调优全解析你有没有遇到过这样的场景&#xff1a;FPGA采集速度明明够快&#xff0c;PCIe链路也支持Gen3 x8&#xff0c;理论带宽近8 GB/s&#xff0c;可实测写入主机内存的速度却只有3 GB/s出头&#xff1f;CPU风扇狂转&#…

作者头像 李华
网站建设 2026/9/24 2:12:53

智能家居中LED显示控制的核心要点解析

手机如何精准掌控家里的LED灯&#xff1f;一文讲透智能家居显示控制的底层逻辑你有没有过这样的体验&#xff1a;晚上回家&#xff0c;打开手机轻轻一点&#xff0c;客厅的灯带缓缓亮起暖白色的光&#xff0c;像有人提前为你点亮了归途&#xff1b;或者在影音室启动“影院模式”…

作者头像 李华
网站建设 2026/9/24 5:40:03

利用Multisim验证克拉泼振荡电路起振条件的详细过程

从零开始验证克拉泼振荡电路的起振条件&#xff1a;Multisim实战全记录你有没有遇到过这种情况——理论课上老师讲得头头是道&#xff0c;什么“巴克豪森准则”、“相位平衡”、“环路增益大于1”&#xff0c;可真到了自己搭电路&#xff0c;却发现压根不起振&#xff1f;输出一…

作者头像 李华
网站建设 2026/9/17 14:19:40

error: c9511e 导致构建失败的项目环境修复

修复 error: c9511e &#xff1a;构建失败的ARM嵌入式环境实战指南 你有没有在凌晨三点准备提交代码时&#xff0c;突然被一条冷冰冰的错误拦住去路&#xff1f; error: c9511e: unable to determine the current toolkit编译中断、构建失败、进度卡死——这不是代码逻辑的…

作者头像 李华
网站建设 2026/9/21 4:40:36

ModbusRTU功能码解析:常用0x03与0x10指令实战案例

深入ModbusRTU&#xff1a;从0x03读取到0x10写入的实战全解析在工业现场&#xff0c;你是否曾遇到这样的场景&#xff1f;一台温控仪数据显示异常&#xff0c;工程师带着笔记本和USB转RS485模块赶到现场&#xff0c;插上线、打开调试工具&#xff0c;却发现读回来的数据是0x000…

作者头像 李华