ES6模块与CommonJS的核心差异在于:ES6模块是静态、编译时解析、浏览器原生支持,具实时绑定和顶层await;CommonJS是动态、运行时加载、Node.js早期标准,依赖缓存与同步require。
JavaScript模块化开发的核心是把代码拆分成独立、可复用、可维护的单元,避免全局污染和依赖混乱。ES6模块(import/export)和CommonJS(require/module.exports)是两种主流方案,它们在设计思想、加载时机、语法特性与运行环境上存在本质差异。
ES6模块是ECMAScript标准定义的官方模块系统,特点是静态分析、只读绑定、顶层作用域、默认严格模式。
import和export必须写在模块顶层,不能出现在条件语句或函数中;工具(如Webpack、Vite)可在构建阶段静态分析依赖关系,实现摇树优化(Tree-shaking)。export let count = 0),所有导入该值的地方都能看到更新(注意:const导出不可重新赋值,但对象属性仍可变)。export default(每个模块最多一个)和export const/name/function(多个),导入时语法对应灵活:import X from './a.js'(默认)、import { foo, bar } from './a.js'(命名)、import * as obj from './a.js'(全部命名导出为命名空间对象)。即可直接运行,自动启用CORS、延迟执行、不共享全局作用域等特性。CommonJS是Node.js早期采用的模块规范,核心是require()同步读取文件并立即执行,返回的是模块导出对象的浅拷贝(实际是引用,但行为上常被理解为“值拷贝”)。
require()可以在任意位置调用,支持表达式路径(如require('./' + name + '.js')),便于按需加载,但无法被静态分析,Tree-shaking困难。require时执行并缓存module.exports对象;后续require直接返回缓存结果,因此模块内逻辑只运行一次。module.exports是一个普通对象,exports只是它的引用别名;重写exports = xxx会断开连接,必须用module.exports = xxx才能完全替换导出内容。module.exports上,require('./a')返回的就是整个module.exports对象,相当于ES6的import * as X from './a';若想模拟默认导出,需显式设置module.exports = function() {...}或module.exports.default = ...。以下是最常被忽略的几个实操区别:
require时返回已执行部分的exports对象(可能不完整);ES6模块则在解析阶段建立绑定,在执行阶段才初始化值,因此即使循环导入也能保持正确引用关系(但需注意初始化顺序)。this是undefined(严格模式);CommonJS中this === module.exports。await支持:ES6模块支持顶层await(Top-level await),可用于异步初始化;CommonJS不支持,必须包裹在async函数中。.mjs扩展名或"type": "module"字段声明;浏览器仅原生支持ES6模块;打包工具(如Webpack、Rollup)通常将CommonJS转译为ES6风格以统一处理。项目选型应结合目标环境和团队规范:
6模块,简洁高效,生态友好。import()动态导入ESM,或配置exports字段在package.json中提供双模块入口("main"指向CommonJS,"module"或"exports"指向ESM)。import和require在同一文件中(语法错误);可通过Babel或TypeScript转译桥接,但建议统一规范。本质上,ES6模块更贴近语言标准与未来演进方向,CommonJS则是历史沉淀下的成熟实践。理解二者差异,不是为了争论优劣,而是为了在调试、打包、跨平台协作时避开陷阱,写出更健壮的模块化代码。