Skip to content

ES Module

ES Module 是 JavaScript 的标准模块方案。它解决的是“代码如何拆分、导入、导出和复用”的问题。

模块解决什么问题

模块化主要用于:

  • 拆分文件职责
  • 显式声明依赖关系
  • 暴露对外接口
  • 避免变量互相污染

在没有模块时,脚本通常直接挂到全局作用域,文件之间的依赖关系也不清晰。

ES Module 的基本模型

一个模块通常做两件事:

  • 通过 export 暴露内容
  • 通过 import 使用其他模块的内容
js
// math.js
export function add(a, b) {
  return a + b
}
js
// main.js
import { add } from './math.js'

console.log(add(1, 2))

模块默认是独立作用域

模块文件有自己的作用域,顶层声明不会自动变成全局变量:

js
const count = 1

上面的 count 只属于当前模块,其他文件不能直接访问,除非显式导出。

静态分析是 ES Module 的关键特征

importexport 都是静态结构,这意味着在代码真正执行前,运行环境就能先分析依赖关系。

这带来几个直接结果:

  • import 必须写在顶层
  • 导入导出关系在解析阶段就能确定
  • 有利于 tree-shaking、依赖预分析和循环依赖处理
js
import { add } from './math.js'

这不是普通函数调用,而是模块语法本身。

浏览器里如何使用模块

浏览器中需要显式声明模块脚本:

html
<script type="module" src="./main.js"></script>

模块脚本和普通脚本有几个常见区别:

  • 自动按模块规则解析 importexport
  • 默认使用严格模式
  • 顶层 thisundefined
  • 同一个模块只会执行一次

模块加载和执行可以怎么理解

可以粗略理解成两个阶段:

  1. 解析依赖图
  2. 按依赖顺序执行模块

在解析阶段,运行环境会先找到所有静态导入,再递归分析整个依赖关系。

导入的不是拷贝值

ES Module 导入的是 live binding,不是简单的值拷贝。

js
// counter.js
export let count = 0

export function increment() {
  count += 1
}
js
// main.js
import { count, increment } from './counter.js'

console.log(count) // 0
increment()
console.log(count) // 1

这说明导入方读到的是导出方当前的绑定结果。

和 CommonJS 的区别

最常见的区别有:

  • ES Module 使用 import / export
  • CommonJS 使用 require() / module.exports
  • ES Module 偏静态
  • CommonJS 偏运行时加载

这也是很多“默认导出行为不一致”“互操作看起来奇怪”的来源。

什么时候适合关注模块边界

在这些场景里更需要明确模块设计:

  • 公共工具函数拆分
  • 大型页面或功能模块拆分
  • 组件库或工具库对外暴露 API
  • 需要按需加载代码

如果只是一个很小的单文件脚本,模块化收益可能不明显。

使用建议

  • 一个模块只承担一类职责。
  • 对外接口尽量稳定,不要把内部细节全部导出。
  • 先分清“模块语法”与“打包工具行为”不是同一层概念。
  • 遇到模块问题时,先确认当前环境到底是浏览器原生 ESM、Node.js ESM,还是打包器处理后的结果。

基于 MIT 许可发布