ES Module规范与特性深度解析
ES Module规范与特性深度解析
一句话概括
ES Module (ESM) 是 JavaScript 官方模块化标准,通过 import / export 语法实现静态化模块化,支持编译时优化(如 Tree Shaking)。
背景
在 ES Module 出现之前,JavaScript 社区主要有两种模块化方案:
- CommonJS:Node.js 采用,同步加载,适合服务端
- AMD/CMD:浏览器端异步加载方案
ES Module 在 ES6 (ES2015) 中正式标准化,成为 JavaScript 官方模块系统,兼顾浏览器和服务端。
ES Module 的优势:
- 静态分析(编译时确定依赖关系)
- 支持 Tree Shaking(删除未使用代码)
- 异步加载(适合浏览器)
- 循环依赖处理更好
- 未来标准(浏览器原生支持)
概念与定义
核心概念
- 模块(Module):一个独立的 JavaScript 文件,默认启用严格模式
- export:导出模块接口(命名导出 / 默认导出)
- import:导入其他模块的导出
- 静态分析:编译时确定模块依赖关系(而非运行时)
模块特性
- 自动严格模式:模块默认
"use strict",无需手动声明 - 模块作用域:顶层变量不会污染全局
- 静态化:
import/export必须位于顶层,不能在条件语句中 - 实时绑定(Live Binding):导出的是值的引用,而非拷贝
最小示例
导出模块(math.js):
1
2
3
4
5
6
7
8
// 命名导出
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// 默认导出
export default {
multiply: (a, b) => a * b
};
导入模块(main.js):
1
2
3
4
5
6
7
8
9
// 导入命名导出
import { add, subtract } from './math.js';
// 导入默认导出
import multiply from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
console.log(multiply.multiply(5, 3)); // 15
核心知识点拆解
1. 导出方式详解
(1)命名导出(Named Export)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方式 1:声明时导出
export const PI = 3.14159;
export function circleArea(r) {
return PI * r * r;
}
// 方式 2:统一导出
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
export { add, subtract };
// 方式 3:重命名导出
export { add as plus, subtract as minus };
特点:
- 可以导出多个值
- 导入时必须使用相同的名称(或可重命名)
- 支持导出时重命名
(2)默认导出(Default Export)
1
2
3
4
5
6
7
8
9
10
11
12
// 方式 1:导出表达式
export default function(a, b) {
return a + b;
}
// 方式 2:导出值
export default 42;
// 方式 3:导出对象
export default {
add: (a, b) => a + b
};
特点:
- 每个模块只能有一个默认导出
- 导入时可以任意命名
- 适合导出模块主功能
(3)混合导出
1
2
3
// math.js
export const PI = 3.14159; // 命名导出
export default (a, b) => a + b; // 默认导出
1
2
3
// main.js
import add, { PI } from './math.js';
console.log(add(PI, PI)); // 6.28318
2. 导入方式详解
(1)导入命名导出
1
2
3
4
5
6
7
8
9
// 导入指定导出
import { add, subtract } from './math.js';
// 重命名导入
import { add as plus } from './math.js';
// 导入所有命名导出(作为对象)
import * as math from './math.js';
console.log(math.add(5, 3));
(2)导入默认导出
1
2
// 默认导出可以任意命名
import myAdd from './math.js';
(3)混合导入
1
import defaultExport, { namedExport1, namedExport2 } from './module.js';
(4)仅执行模块(不导入)
1
import './polyfill.js'; // 执行副作用代码
3. 静态分析 vs 动态加载
静态导入(Static Import)
1
2
3
4
5
6
7
// ✅ 正确:顶层导入
import { add } from './math.js';
// ❌ 错误:不能在条件语句中
if (condition) {
import { add } from './math.js'; // SyntaxError
}
优势:
- 编译时确定依赖关系
- 支持 Tree Shaking
- 静态分析工具优化
动态导入(Dynamic Import)
1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 正确:动态导入返回 Promise
if (condition) {
import('./math.js').then(module => {
console.log(module.add(5, 3));
});
}
// 使用 async/await
async function loadModule() {
const math = await import('./math.js');
console.log(math.add(5, 3));
}
使用场景:
- 条件加载
- 按需加载(路由懒加载)
- 运行时确定模块路径
4. 实时绑定(Live Binding)
ES Module 导出的是值的引用(实时绑定):
1
2
3
4
5
6
// counter.js
export let count = 0;
export function increment() {
count++;
}
1
2
3
4
5
6
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1(值已更新!)
对比 CommonJS(值拷贝):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CommonJS
// counter.js
let count = 0;
module.exports = { count, increment };
function increment() {
count++;
}
// main.js
const { count, increment } = require('./counter.js');
console.log(count); // 0
increment();
console.log(count); // 0(还是 0!值拷贝)
5. Tree Shaking 原理
Tree Shaking 是删除未使用代码的优化技术,依赖 ES Module 的静态分析。
示例:
1
2
3
4
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
1
2
3
4
// main.js
import { add } from './math.js'; // 只导入 add
console.log(add(5, 3));
打包结果(经过 Tree Shaking):
1
2
3
// multiply 和 subtract 被删除(未使用)
const add = (a, b) => a + b;
console.log(add(5, 3));
CommonJS 无法 Tree Shaking(运行时加载):
1
2
// CommonJS 无法通过静态分析确定哪些导出被使用
const { add } = require('./math.js'); // 运行时才知道导入什么
实战案例
案例 1:创建一个工具库(ES Module 版本)
项目结构:
1
2
3
4
5
6
7
my-utils/
├── package.json
├── src/
│ ├── string.js
│ ├── array.js
│ └── object.js
└── index.js
package.json:
1
2
3
4
5
6
7
8
{
"name": "my-utils",
"type": "module",
"main": "./index.js",
"exports": {
".": "./index.js"
}
}
src/string.js:
1
2
3
4
5
export const capitalize = (str) =>
str.charAt(0).toUpperCase() + str.slice(1);
export const camelCase = (str) =>
str.replace(/-(\w)/g, (_, c) => c.toUpperCase());
src/array.js:
1
2
3
4
5
6
export const unique = (arr) => [...new Set(arr)];
export const flatten = (arr) =>
arr.reduce((flat, item) =>
flat.concat(Array.isArray(item) ? flatten(item) : item), []
);
index.js(统一导出):
1
2
3
export * from './src/string.js';
export * from './src/array.js';
export * from './src/object.js';
使用:
1
2
3
4
import { capitalize, unique } from 'my-utils';
console.log(capitalize('hello')); // Hello
console.log(unique([1, 2, 2, 3])); // [1, 2, 3]
案例 2:动态导入实现路由懒加载
React 路由懒加载(使用 React.lazy):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { lazy, Suspense } from 'react';
// 动态导入组件
const Home = lazy(() => import('./pages/Home.js'));
const About = lazy(() => import('./pages/About.js'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
Vue 路由懒加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createRouter } from 'vue-router';
const routes = [
{
path: '/',
component: () => import('./pages/Home.vue') // 动态导入
},
{
path: '/about',
component: () => import('./pages/About.vue')
}
];
const router = createRouter({
routes
});
案例 3:循环依赖的处理
ES Module 能更好地处理循环依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('在 a 中,b =', b); // 'b'(已完成执行)
// b.js
import { a } from './a.js';
export const b = 'b';
console.log('在 b 中,a =', a); // 'a'(实时绑定,即使 a 还未执行完)
// main.js
import { a, b } from './a.js';
console.log(a, b); // 'a', 'b'
执行结果:
1
2
3
在 b 中,a = a(实时绑定,能访问)
在 a 中,b = b
a b
原因:
- ES Module 使用实时绑定(Live Binding)
- 即使模块未执行完,也能访问导出的绑定
底层原理
模块加载流程(浏览器)
- 解析(Parsing):解析
import/export语句,构建模块依赖图 - 加载(Loading):递归加载所有依赖模块
- 连接(Linking):建立模块间的引用关系(实时绑定)
- 执行(Evaluation):执行模块代码
模块记录(Module Record)
浏览器内部用 模块记录 表示模块:
1
2
3
4
5
6
ModuleRecord {
status: 'unlinked' | 'linking' | 'linked' | 'evaluating' | 'evaluated',
dependencies: [...], // 依赖的模块
exportNames: [...], // 导出的名称
evalutationCode: ... // 模块代码
}
实时绑定的实现
1
2
3
4
5
6
7
8
9
// 伪代码
Module {
exports: {
count: <绑定到原始变量的引用>
}
}
// 当访问 module.exports.count 时
// 实际上访问的是原始变量(实时更新)
高频面试题解析
Q1: ES Module 和 CommonJS 的核心区别?
答:
| 特性 | CommonJS | ES Module |
|---|---|---|
| 加载方式 | 同步加载(运行时) | 异步加载(编译时静态分析) |
| 导出方式 | module.exports / exports | export / export default |
| 导入方式 | require() | import / import() |
| 值传递 | 值拷贝(缓存) | 实时绑定(Live Binding) |
| 静态分析 | 不支持(运行时加载) | 支持(编译时解析) |
| Tree Shaking | 不支持 | 支持 |
| 循环依赖 | 可能返回不完整模块 | 更好的支持(实时绑定) |
| 使用场景 | Node.js 服务端 | 浏览器 + 服务端(通用) |
示例:值拷贝 vs 实时绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// CommonJS(值拷贝)
// counter.js
let count = 0;
module.exports = { count, increment };
function increment() { count++; }
// main.js
const { count, increment } = require('./counter');
console.log(count); // 0
increment();
console.log(count); // 0(值拷贝,不会更新)
// ============================================
// ES Module(实时绑定)
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1(实时绑定,已更新)
Q2: 为什么 ES Module 支持 Tree Shaking,而 CommonJS 不支持?
答:
- ES Module 是静态的:
import/export必须在顶层,编译时就能确定依赖关系 - CommonJS 是动态的:
require()可以写在条件语句、函数内,运行时才知道导入什么
示例:
1
2
3
4
5
6
// ES Module(静态,可 Tree Shaking)
import { add } from './math.js'; // 编译时确定只导入 add
// CommonJS(动态,无法 Tree Shaking)
const moduleName = condition ? './math.js' : './other.js';
const { add } = require(moduleName); // 运行时才知道导入什么
Q3: 动态导入(import())和静态导入(import)的区别?
答:
| 特性 | 静态导入(import) | 动态导入(import()) |
|---|---|---|
| 位置 | 必须顶层 | 可以在任何地方 |
| 加载时机 | 编译时 | 运行时 |
| 返回值 | 无(直接导入) | Promise |
| 是否异步 | 否 | 是 |
| 使用场景 | 常规导入 | 按需加载、条件加载 |
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 静态导入(编译时确定)
import { add } from './math.js';
// 动态导入(运行时加载)
if (needMath) {
import('./math.js').then(module => {
console.log(module.add(5, 3));
});
}
// 使用 async/await
async function loadMath() {
const math = await import('./math.js');
console.log(math.add(5, 3));
}
Q4: 如何在 Node.js 中使用 ES Module?
答:
方式 1:package.json 中声明 "type": "module"
1
2
3
4
5
6
// package.json
{
"name": "my-app",
"type": "module",
"main": "index.js"
}
1
2
3
// index.js(可以使用 import/export)
import fs from 'fs';
export const hello = 'world';
方式 2:文件后缀使用 .mjs
1
2
// module.mjs
export const foo = 'bar';
1
2
3
// main.mjs
import { foo } from './module.mjs';
console.log(foo);
方式 3:在 CommonJS 中动态导入 ES Module
1
2
3
4
5
// CommonJS 模块
(async () => {
const { foo } = await import('./module.mjs');
console.log(foo);
})();
Q5: export * 和 export default 的区别?
答:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 命名导出(export)
export const a = 1;
export const b = 2;
// 导入时必须使用相同名称(可重命名)
import { a, b } from './module.js';
import { a as A } from './module.js';
// ============================================
// 默认导出(export default)
export default 42;
// 导入时可以任意命名
import myNumber from './module.js';
混合使用:
1
2
3
4
5
6
7
// module.js
export const PI = 3.14;
export default (a, b) => a + b;
// main.js
import add, { PI } from './module.js';
console.log(add(PI, PI));
总结与扩展
核心要点
- ES Module 是 JavaScript 官方标准
- 使用
import/export语法 - 静态化设计,编译时确定依赖
- 使用
- 两种导出方式
- 命名导出:
export const foo = ... - 默认导出:
export default ...
- 命名导出:
- 实时绑定(Live Binding)
- 导出的是值的引用(而非拷贝)
- 循环依赖处理更好
- 支持 Tree Shaking
- 静态分析未使用代码
- 删除死代码,减小打包体积
- 动态导入(import())
- 返回 Promise
- 支持按需加载、条件加载
扩展学习
- Module Record 规范:ECMA-262 模块记录定义
- 打包工具原理:Webpack / Rollup 如何处理 ES Module
- 浏览器原生 ESM:
<script type="module">的使用 - Node.js 双模块系统:CommonJS 与 ES Module 互操作
实战建议
- 优先使用 ES Module
- 新项目统一使用 ESM
- 老项目逐步迁移
- 利用 Tree Shaking 优化打包体积
- 使用命名导出(便于静态分析)
- 避免默认导出的整个对象
- 合理使用动态导入
- 路由懒加载
- 大型库按需加载
- 注意浏览器兼容性
- 旧浏览器使用打包工具(Babel / Webpack)
- 现代浏览器可原生支持 ESM
参考资料:
- MDN - ES Module
- ECMA-262 规范 - Module Record
- Node.js 官方文档 - ES Module
- 《深入浅出 ES Module》 - David Flanagan
本文由作者按照 CC BY 4.0 进行授权