标准答案

  1. CommonJS 使用 require 和 module.exports,主要是运行时、同步加载。require 可以写在条件语句或函数里,执行到这一行时才加载模块,更适合 Node.js 传统运行环境。
  2. ES Module 使用 import 和 export,语法是静态的,静态 import 必须写在模块顶层。这个特性让构建工具可以在编译期分析依赖关系,做 Tree Shaking、预加载和循环依赖处理。
  3. CommonJS 导出的是当时 module.exports 上的值或对象引用;ESM 的导入是 live binding,导入方拿到的是导出变量的绑定,导出方变量变化后,导入方读取到的值也会更新。
  4. 动态 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。

作者信息