es modules底层实现分为构造、实例化、求值三阶段:构造阶段静态解析import生成module record并构建依赖图;实例化阶段分配内存并建立import/export的live binding链接;求值阶段按拓扑序执行顶层代码填充变量值。

要看 ES Modules 的底层实现原理,重点不是翻 V8 或浏览器源码(那太重),而是理解规范定义的三阶段流程——构造、实例化、求值——以及每个阶段浏览器实际做了什么。这些阶段在规范里写得清楚,在 Chrome、Firefox 的引擎中也严格遵循。
构造阶段:构建依赖图 + 生成 Module Record
浏览器拿到 <script type="module" src="main.js"></script> 后,第一步不是执行,而是静态扫描所有 import 语句:
- 解析模块标识符(Module Specifier),比如
'./utils.js'被转为绝对 URL; - 发起并行网络请求下载所有依赖模块(含嵌套依赖),同时把路径加入模块缓存表(key 是解析后的 URL,value 初始为
fetching); - 下载完成后,对 JS 源码做**静态解析**(不运行),生成
Module Record,它包含:RequestedModules(依赖列表)、ImportEntries(导入项映射)、ExportEntries(导出项映射)、AST 等元信息; - 这个阶段完全不执行任何代码,也不分配变量内存,纯“画地图”。
实例化阶段:内存分配 + live binding 链接
所有模块的 Module Record 齐备后,进入链接(linking):
- 为每个模块的
export变量在内存中分配存储位置(注意:此时仍是未初始化状态,比如let x是uninitialized); - 将所有
import绑定到对应export的内存地址——这就是 Live Binding 的本质:不是拷贝值,而是共享同一块内存引用; - 如果存在循环依赖,实例化仍能完成(因为只分配地址,不取值),但求值时可能遇到
undefined(取决于执行顺序)。
求值阶段:按拓扑序执行顶层代码
实例化完成后,浏览器按依赖拓扑排序(从叶子模块到入口)依次执行每个模块的顶层语句:
- 执行
export let count = 0→ 把0写入之前分配的内存位置; - 执行函数声明、类声明等,填充对应内存槽;
- 一旦某个模块求值完成,它的
export值就真实可用了,其他已实例化的模块能立刻读到最新值(因共享地址); - 这个阶段才真正“跑代码”,也是唯一可能发生运行时错误(如 ReferenceError)的地方。
关键细节不能跳过
理解底层,这几个点常被忽略但决定行为差异:
- 模块缓存是 per-URL:同一 URL 不管被 import 多少次,只构造/实例化/求值一次;不同 URL(哪怕内容相同)算不同模块;
- import() 是动态的,走另一套流程:它返回 Promise,触发异步构造+实例化+求值,不参与静态依赖图;
-
ESM 默认严格模式:无需
'use strict',顶层 this 是 undefined,禁止静默失败; - 与 CommonJS 根本不同:CJS 是运行时同步加载对象,ESM 是编译时静态分析+异步加载+内存地址绑定。











