← 返回题目列表

ES Module 和 CommonJS 有什么区别?

高频 中等 第 9 / 28 题 更新于 2026/07/27
ES6ModuleCommonJS模块化

简化版

ES Module 使用 import/export,是静态模块系统,支持 Tree Shaking,导出是 live binding;CommonJS 使用 require/module.exports,主要运行时加载,导出更像值拷贝或对象引用。浏览器原生支持 ESM,Node 同时支持两套体系。

详细版

ESM:

import { foo } from './foo.js';
export const bar = 1;

CommonJS:

const foo = require('./foo');
module.exports = { bar: 1 };

关键区别:

  • ESM 静态分析,编译阶段能知道依赖关系。
  • CommonJS 运行时加载,可以写在条件分支里。
  • ESM 导入是只读 live binding。
  • CommonJS 默认同步加载,更适合早期 Node。

打包工具能利用 ESM 做 Tree Shaking,删除未使用导出。

完整版教学

一、静态和动态的差别

ESM 的 import 必须在顶层,不能随便写在 if 里:

import { a } from './a.js';

这让工具在代码执行前就能分析模块依赖。CommonJS 的 require 是函数调用:

if (condition) {
  require('./a');
}

灵活,但静态优化更困难。

二、live binding 是什么

ESM 导入的不是普通复制值,而是导出变量的实时绑定。如果导出模块内部更新了变量,导入方读到的是新值。

同时导入绑定是只读的,不能在导入方直接改。

三、循环依赖表现不同

ESM 因为有静态结构和 live binding,对循环依赖处理相对更可预测。CommonJS 在循环依赖中可能拿到未执行完模块的半成品 exports。

面试中不必展开太深,但要知道循环依赖是模块系统的重要差异点。

四、工程中的选择

现代前端项目基本使用 ESM,便于浏览器、Vite、Rollup、Webpack 优化。Node 老项目仍有大量 CommonJS,新项目则越来越多使用 ESM。

混用时要注意默认导出、命名导出、文件扩展名、type: module 等兼容问题。

五、面试追问与工程落地

模块化常见追问是“ESM 为什么能 Tree Shaking”。因为 import/export 是静态结构,打包器能在构建阶段分析哪些导出没有被使用,再配合副作用标记删除无用代码。CommonJS 的 require 更动态,静态分析难度更大。

另一个追问是“ESM 导入能不能改”。导入绑定是只读的,不能在导入方重新赋值。但如果导入的是对象,对象内部属性是否能改,要看对象本身是否被冻结。只读的是绑定,不一定是深层数据。

工程里要注意副作用模块,比如只 import 一个 CSS、polyfill 或全局注册逻辑:import './setup'。这种模块即使没有导出也不能随便删除。库作者通常通过 package.jsonsideEffects 告诉打包器哪些文件有副作用。

六、用链接、求值和缓存解释模块行为

ESM 的静态 importexport 让引擎在执行前建立模块依赖和绑定关系,随后才进入求值阶段;动态 import() 则是在运行期返回 Promise 的独立能力。导入名称是只读的 live binding:导出模块更新该绑定后,导入方下次读取可看到新值,但导入方不能给它重新赋值。

// counter.js
export let count = 0
export const inc = () => count++
// main.js
import { count, inc } from './counter.js'
inc()
console.log(count) // 1
维度ESMCommonJS
依赖声明静态 import;另有动态 import()运行期 require()
导入语义只读 live binding获得 module.exports 当前导出值
常见加载方式支持异步链接和顶层 await传统 require 同步执行
优化基础静态图便于 tree shaking动态写法较难静态分析

CommonJS 模块首次 require 后通常按解析后的文件名缓存;再次加载常返回缓存中的 module.exports。这不等于“所有导入都是值拷贝”:若导出的是对象,消费者仍能观察同一对象的属性变更;若先解构出一个原始值,则该本地变量不会自动跟随之后的重新赋值。

循环依赖不是由某一种模块系统自动解决的。ESM 可能在初始化前访问绑定而触发暂时性死区错误,CommonJS 则可能拿到尚未执行完的部分导出;降低顶层副作用、把共享协议抽离为第三个模块,通常比依赖执行顺序更可靠。

Tree shaking 是“静态结构让优化成为可能”,不是 ESM 语法的必然结果;打包器还要结合使用情况和副作用信息才能安全删除代码。

七、常见误区与追问

  • 误区:ESM 完全不能动态加载模块。 静态 import 需位于模块顶层,但 import() 可以按运行条件异步加载。
  • 误区:CommonJS 导入的一定是不可变化的值拷贝。 require 返回导出值;共享对象的内部变化仍可见,具体表现取决于消费方式。
  • 误区:只要改成 ESM 就一定能完成 tree shaking。 动态访问和有副作用的模块都会限制删除,最终效果取决于工具配置与代码形态。
  • 追问:live binding 为什么不能在导入方赋值? 导入方获得的是导出绑定的只读视图,所有权仍在导出模块。
  • 追问:exports.x 和 module.exports 是什么关系? CommonJS 初始时 exports 指向 module.exports;给 exports 整体重新赋值不会自动改变真正的导出对象。
  • 追问:模块为什么通常只执行一次? 加载器会缓存已求值的模块实例,后续导入复用它;不同 URL 或解析结果可能形成不同实例。
  • 追问:怎样降低循环依赖风险? 抽离共同依赖、减少顶层执行和延迟读取具体值,并用测试覆盖真实加载顺序。

八、加强记忆

ESM 是“静态、可分析、live binding”,CommonJS 是“运行时、同步 require、exports 对象”。前端工程优先 ESM,因为它更利于构建优化。

分析模块题时按解析与链接、求值、缓存三个阶段展开,再讨论 live binding 和循环依赖,因果关系会更清楚。