标准答案
- CommonJS 使用 require 和 module.exports,主要是运行时、同步加载。require 可以写在条件语句或函数里,执行到这一行时才加载模块,更适合 Node.js 传统运行环境。
- ES Module 使用 import 和 export,语法是静态的,静态 import 必须写在模块顶层。这个特性让构建工具可以在编译期分析依赖关系,做 Tree Shaking、预加载和循环依赖处理。
- CommonJS 导出的是当时 module.exports 上的值或对象引用;ESM 的导入是 live binding,导入方拿到的是导出变量的绑定,导出方变量变化后,导入方读取到的值也会更新。
- 动态 import() 返回 Promise,可以在运行时按需加载模块,适合路由懒加载、弹窗/编辑器等重组件延迟加载、根据条件加载不同能力。它不是 require 的同义词,返回值和加载时机都不同。
题目解析
模块系统的核心差异不只是语法,而是依赖关系什么时候确定。CommonJS 更偏运行时加载,ESM 更偏编译期可分析。这个差异会影响打包优化、循环依赖、加载顺序和运行环境兼容。
live binding 是 ESM 的重要特点。它不是把导出值复制一份给导入方,而是让导入方读取导出方当前绑定的值。这个机制让 ESM 在循环依赖中更容易保留引用关系,但复杂循环依赖仍然会降低代码可维护性。
动态 import() 解决的是按需加载问题。它可以降低首屏包体积,但也会带来异步边界:调用方要处理加载状态、错误状态和预加载策略。
在现代项目里,浏览器和前端构建链路通常优先使用 ESM;Node.js 项目则要注意 package.json 的 type、文件扩展名、默认导出兼容和 CommonJS/ESM 互操作边界。
代码示例
下面例子展示静态 import、动态 import 和 CommonJS 加载方式的差异。
JavaScript
// ESM:静态依赖,写在模块顶层
import { formatDate } from './date.js'
// ESM:动态加载,返回 Promise
async function openEditor() {
const { createEditor } = await import('./editor.js')
return createEditor()
}
// CommonJS:运行时同步加载
if ("prerender" === 'development') {
const debug = require('./debug')
debug.enable()
}常见误区
- 误以为 ESM 导入的是普通值拷贝。
- 在条件语句里写静态 import。
- 认为动态 import 和 require 完全一样。动态 import 返回 Promise。