es模块执行分解析→实例化→执行三阶段:解析构建静态依赖图;实例化建立导出导入的实时绑定;执行按依赖顺序单向运行顶层代码,且默认defer、作用域隔离。

模块脚本(type="module")在浏览器中不是“下载完就执行”,而是严格遵循解析(Parsing)→ 实例化(Instantiation)→ 执行(Execution)三个解耦阶段。这三步彼此独立、不可跳过,共同构成 ES 模块的底层生命周期。
第一阶段:解析 —— 构建静态依赖图
浏览器拿到模块文件(如 index.js)后,不运行任何代码,只做一件事:扫描所有顶层 import 语句,递归收集全部依赖路径,生成一张完整的模块依赖图(Module Graph)。
- 只识别静态 import(
import { x } from './a.js'),忽略动态import()和条件语句里的导入 - 路径必须显式:带
./或../前缀,且含扩展名(如utils.js),否则直接报错 - 裸导入(如
import 'lodash')在此阶段失败,除非已通过<script type="importmap"></script>提前声明映射 - 多个模块并行解析,已解析过的模块不再重复处理
第二阶段:实例化 —— 分配内存与绑定导出
依赖图确定后,浏览器为每个模块分配执行环境,并建立导出与导入之间的符号绑定(live binding),但此时仍不执行任何语句。
- 每个模块生成一个“导出映射表”,记录哪些变量/函数被
export出来 - 导入语句(如
import { foo } from './a.js')指向的是这张表中的引用,而非拷贝值 - 如果两个模块都导入同一个模块,它们共享同一份导出绑定(修改
a.js中的export let x = 1,所有导入方看到的x都同步更新) - 此阶段失败(例如循环依赖未正确处理)会导致整个模块图加载中止
第三阶段:执行 —— 自上而下运行顶层代码
只有当整个依赖图完成实例化后,浏览器才开始真正执行代码,且严格按依赖顺序,从入口模块开始,深度优先执行。
- 执行是单向的:先执行所有被依赖的模块(如
a.js),再执行依赖它的模块(如index.js) - 模块顶层代码只执行一次,后续导入直接复用已执行结果(缓存行为)
- 模块脚本默认
defer:执行时机等同于DOMContentLoaded,且多个type="module"脚本严格按 HTML 中出现顺序执行,不受下载快慢影响 - 模块作用域隔离:顶层声明的
const、function不挂载到window,document.currentScript在模块内始终为null
这三阶段设计让模块具备可预测性、可缓存性和强隔离性,也解释了为什么写错路径、漏写扩展名、或在非模块上下文中用 import 会直接卡在解析或实例化环节,而不是等到执行时才报错。











