JavaScript模块化开发核心是拆分代码为独立可复用单元,ES6模块(静态解析、顶层导入、实时绑定)与CommonJS(动态加载、运行时require、值拷贝)在语法、时机、环境和行为上存在本质差异,二者不可混用但可通过工具桥接。

JavaScript模块化开发的核心是把代码拆分成独立、可复用的单元,ES6模块(import/export)和CommonJS(require/module.exports)是两种主流方案,它们在语法、加载时机、运行环境和行为逻辑上有本质差异。
ES6模块是编译时静态解析,依赖关系在代码分析阶段就确定
ES6模块的import必须写在文件顶层,不能放在条件语句或函数中;所有导入导出语句都会被提前“提升”,但不会执行模块体——直到真正被调用。这种静态特性让工具(如Webpack、Vite)能做摇树优化(Tree Shaking),自动剔除未使用的导出内容。
-
必须用字面量字符串指定模块路径:不支持动态拼接路径,例如
import('./utils/' + name + '.js')会报错(但可用import()动态导入替代) -
默认导出和具名导出分离明确:一个模块可同时有
export default和多个export const xxx,导入时语法也对应区分 - 绑定是实时的、只读的:导入的变量与原始模块中的变量保持绑定(类似引用),且不可重新赋值(但对象属性仍可修改)
CommonJS是运行时动态加载,模块输出是值的拷贝
Node.js早期采用CommonJS规范,require()可以在任意位置调用,支持条件加载、循环引用处理更灵活。每次require返回的是module.exports对象的一个浅拷贝(实际是缓存引用),模块代码在第一次require时同步执行。
-
模块输出是对象的属性:无论你导出什么,最终都挂载到
module.exports上;exports只是它的快捷引用,直接赋值exports = {}会断开连接 -
可以动态决定加载哪个模块:比如
if (env === 'dev') require('./dev-tools')完全合法 -
循环引用返回当前已执行的部分:A require B,B 又 require A,B 中拿到的是 A 当前已执行完的
exports对象,不是空对象也不是完整对象
两者不能混用,但可通过工具桥接
ES6模块和CommonJS模块语法不兼容,直接在Node中用import加载.cjs文件或反之会报错。不过现代Node(v14+)通过"type": "module"字段或.mjs扩展名启用ESM支持;而打包工具如Webpack、Rollup能自动识别并转换两者。
立即学习“Java免费学习笔记(深入)”;
-
Node中混合使用需注意文件后缀和package.json配置:.mjs强制ESM,.cjs强制CommonJS,.js则取决于
"type"字段 -
ESM中想用CommonJS模块,可以直接
import,Node会自动包装:如import _ from 'lodash'没问题,但lodash本身是CommonJS,Node内部做了适配 -
CommonJS中想用ESM模块,只能用
await import():因为require无法加载ESM,异步动态导入是唯一方式
实际项目中怎么选?看环境和目标
浏览器原生支持ES6模块(),现代前端工程基本统一用ES6语法;Node服务端早期多用CommonJS,但新项目推荐启用ESM。二者共存是过渡常态,关键在于理解差异,避免踩坑。
-
写库时建议同时提供两种格式:用
exports字段在package.json中分别指定"import"和"require"入口 -
调试时注意模块类型报错信息:如
Cannot use import statement outside a module说明当前上下文是CommonJS,需检查文件类型或运行参数 -
不要手动修改
exports和module.exports混用:比如先exports.a = 1再module.exports = {b: 2}会导致前者丢失











