ES Module 规范与特性
一句话概括
ES Module 是 JavaScript 语言层面的官方模块系统——静态结构支持 Tree Shaking、实时绑定解决循环依赖、异步加载原生跑在浏览器,三个特性加起来根本不是 CJS 的语法糖,而是换了赛道。
核心知识点
1. 语法全景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ── 导出 ──
export const PI = 3.14;
export function add(a, b) { return a + b; }
// 统一导出
export { PI, add };
// 重导出
export { add as sum } from './math.js';
// 默认导出(一个模块只能一个)
export default function () { /* ... */ }
// ── 导入 ──
import { add, PI } from './math.js';
import { add as plus } from './math.js'; // 重命名
import * as math from './math.js'; // 全量
import def from './math.js'; // 默认导入
import def, { add } from './math.js'; // 混合
// ── 动态导入 ──
const { heavy } = await import('./heavy.js');
2. Live Binding — 和 CJS 的根本区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// counter.js
export let count = 0;
export const inc = () => { count++; };
// main.js
import { count, inc } from './counter.js';
console.log(count); // 0
inc();
console.log(count); // 1 ← 实时绑定!
// 对比 CommonJS:
// const { count, inc } = require('./counter');
// inc();
// console.log(count); // 0 ← 值拷贝,不会变
原理: ESM 的 export/import 不是”把值拷过去”,而是建立了变量地址的映射。import { count } 相当于创建了一个指向原模块 count 变量的引用。inc 改了原值,import 端立刻看到。
3. 静态结构 → Tree Shaking
1
2
3
4
5
6
7
8
9
// utils.js
export const used = () => 'used';
export const unused = () => 'unused';
// app.js
import { used } from './utils.js';
// 打包后(Rollup / Webpack):
// unused 的代码直接被删除,产物里没有它
为什么 CJS 做不到? require 可以在 if 里、循环里、任何地方调用——它是运行时的。打包工具不知道哪些导出会被用到。而 ESM 的 import 只能是顶层静态声明,编译时就能画出完整依赖图。
4. 动态 import — 按需加载
1
2
3
4
5
6
7
8
9
10
11
12
// 静态 import 不能放条件分支
// import { heavy } from './heavy.js'; // ❌ 不能放 if 里
// 动态 import 返回 Promise,随时随地调用
if (condition) {
const { heavy } = await import('./heavy.js');
heavy();
}
// Vue Router / React Router 的路由懒加载就是靠这个
const Profile = () => import('./pages/Profile.vue');
const LazyPage = React.lazy(() => import('./Page.tsx'));
5. ESM vs CJS 速查
| CommonJS | ES Module | |
|---|---|---|
| 加载时机 | 运行时 | 编译时解析,执行时取值 |
| 加载方式 | 同步阻塞 | 异步 |
| 导出 | 值拷贝 | 实时绑定(引用) |
| Tree Shaking | ❌ 条件 require 无法静态分析 | ✅ |
| 循环依赖 | 拿到不完整的对象 | 引用不丢,但值可能是 undefined |
| 顶层 await | ❌ | ✅ (ES2022) |
| 浏览器原生 | ❌ 需打包 | ✅ |
| Node.js | 默认 | .mjs 或 "type": "module" |
其实你每天都在用
import React from 'react'— 你的源码写 ESM,babel / tsc 转成 CJS 或保留 ESM- Vite 开发服务器 — 直接用浏览器原生
<script type="module">加载,不打包、秒启动 - 路由懒加载 —
() => import('./Home.vue')把应用拆成独立 chunk,首屏只加载需要的 - npm 包的
"exports"和"module"字段 — 告诉打包工具 ESM 入口在哪,优先走 ESM - Tree Shaking 减小包体积 — Lodash 用
import { debounce } from 'lodash-es'代替import _ from 'lodash'
常见误解
❌ 误区:「import 是同步的」 恰恰相反。浏览器中
<script type="module">默认是异步 + defer 执行。Node 中 ESM 也是异步加载。CJS 的 require 才是真正同步阻塞的(读文件 + 执行)。❌ 误区:「export default 和 export const 没啥区别」 有。
export default没有绑定名字,导入方自由命名——IDE 重构时追踪困难、Tree Shaking 也会受影响。社区共识:除非模块只有一个主功能,否则优先用命名导出。❌ 误区:「ESM 在 Node 可以直接 import CJS,所以互操作没问题」 方向是单向的。ESM 可以 import CJS(Node 做了一层包装),但 CJS 不能 require ESM。项目里 CJS 和 ESM 混用时,注意依赖方向别搞反。
❌ 误区:「Tree Shaking 是 Rollup/Webpack 的特性,和 ESM 没关系」 打包工具的 Tree Shaking 必须建立在 ESM 静态结构之上。CJS 代码根本无法安全 Tree Shake——因为 require 可以出现在任何地方,工具不知道哪些代码是 dead code。
一句话总结
ES Module 的静态分析让打包工具有了上帝视角,Live Binding 让循环依赖不再断裂,异步加载让浏览器原生模块成为现实——这才是语言级模块系统的真正意义。