ES6模块导入导出方式
ESM 的 import / export 语法与模块边界。
#dev
#concept
#growing
#javascript
#ecmascript
[!info] related notes
- 关系总览: ECMAScript 模块模式;所属 MOC: ECMAScript MOC, ES6 新特性 MOC
- 并列概念: commonjs-amd-umd, HTML 中的 JS 脚本
- Node 配置: package-json
- 关系笔记: ECMAScript 模块模式, JavaScript的工程化与运行时
ES6 模块 (ESM)
一句话定义
ESM 是 ECMAScript 原生的模块系统,通过静态的 import / export 语法实现模块间依赖声明,支持静态分析和 Tree Shaking。
核心机制 / 工作原理
- 被宿主按 ESM 加载的文件具有独立模块作用域,并自动采用严格模式;普通脚本文件并不因此自动成为模块。
- 在同一加载环境中,同一模块标识通常只求值一次;Node ESM 按 URL 缓存,不同查询参数或片段可形成不同实例。
- 静态
import声明让依赖可在执行前分析;模块解析、加载与求值仍由宿主完成。Tree Shaking 是工具优化,还受副作用和配置影响。 - ESM 导出的是 活绑定 (live bindings):导入方读取的是关联的导出绑定,不能直接给这个导入绑定赋值;导出对象的属性是否可改是另一回事。
命名导出 / 导入
// ---- math.js ----
export const PI = 3.14159;
export function add(a, b) { return a + b; }
// ---- app.js ----
import { PI, add } from './math.js';
import { add as sum } from './math.js'; // 重命名导入
import * as math from './math.js'; // 命名空间导入
默认导出 / 导入
// ---- logger.js ----
export default function log(msg) {
console.log(msg);
}
// ---- app.js ----
import log from './logger.js'; // 名字可以随便取
import myLog from './logger.js';
重新导出 (Re-export)
export { add } from './math.js'; // 转发命名导出
export { default as Logger } from './logger.js'; // 转发默认导出
export * from './math.js'; // 转发所有命名导出(不含 default)
export * as math from './math.js'; // ES2020: 命名空间再导出
动态导入
const module = await import('./heavy-module.js');
module.doSomething();
// 条件导入
if (needsChart) {
const { Chart } = await import('./chart.js');
new Chart(canvas);
}
活绑定 (Live Bindings)
// counter.js
export let count = 0;
export function increment() { count++; }
// app.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 — 活绑定,看到的是最新值
循环依赖行为
ESM 会先建立模块之间的绑定,再按依赖求值。循环本身不必报错,但访问尚未初始化的 let / const / class 绑定会触发暂时性死区错误,不会返回 CJS 式的“部分导出对象”。
// a.mjs
import { b } from './b.mjs';
export const a = 'a';
console.log('b in a:', b);
// b.mjs
import { a } from './a.mjs';
export const b = 'b';
console.log('a in b:', a);
分别保存为两个文件后运行 node a.mjs:b.mjs 读取 a 时抛出 ReferenceError,两条日志不会按原例注释正常输出。把读取延后到初始化完成之后,或消除这条循环,才可能安全执行。
ESM vs CJS 快速对比
| 维度 | ESM | CJS |
|---|---|---|
| 语法 | import / export | require() / module.exports |
| 依赖表达 | 静态声明,另支持 import() | require() 运行时调用 |
| 导入结果 | 活绑定或模块命名空间 | module.exports 值,对象不自动复制 |
| Tree Shaking | 静态结构利于工具分析,仍受副作用影响 | 分析较难,能力依工具而定 |
| 浏览器原生 | 支持(<script type="module">) | 不支持 |
| 循环依赖 | 提前读取未初始化绑定会报错 | 可能拿到不完整导出 |
边界与常见误解
import必须在顶层:常规import不能在if或函数内使用(静态分析限制),条件加载请用import()。- 默认导出不是命名导出的语法糖:一个模块可以同时有默认导出和命名导出,它们是不同的导出槽位。
- Node.js 中 ESM 需要配置:明确方式是使用
.mjs,或为.js在最近的package.json中设置"type": "module";较新 Node 也可能进行语法检测,不宜依赖隐式判定。
示例运行前提
前面的 .js 文件示例按文件注释分别保存,并在 Node 项目中设置 "type": "module";动态导入部分是接口示意,需自行提供对应模块和变量。循环示例使用 .mjs,不依赖包配置。