JavaScript高级程序设计笔记 26
第 26 章 模块
学习目标:理解 JavaScript 模块化的动机、早期模块方案和 ES6 模块系统,掌握导入、导出、循环依赖与加载行为。
1. 本章核心脉络
模块化解决的是大型 JavaScript 程序如何拆分、复用、组织和加载代码的问题。没有模块系统时,脚本之间共享全局作用域,容易命名冲突、依赖混乱、加载顺序脆弱。
本章主线:
- 模块模式的基本概念。
- ES6 之前的模块方案:CommonJS、AMD、UMD。
- ES6 模块:
import、export、静态分析、异步加载、浏览器支持。
2. 理解模块模式
2.1 模块是什么
模块是一个相对独立的代码单元,通常具备:
- 私有作用域。
- 明确的依赖。
- 明确的导出接口。
- 可被其他模块复用。
模块化的价值:
- 降低全局污染。
- 提升可维护性。
- 让依赖关系可追踪。
- 方便测试和复用。
- 支持构建工具做静态分析和优化。
2.2 模块标识符
模块标识符用于定位模块。
常见形式:
import util from "./util.js";
import React from "react";
可能是:
- 相对路径。
- 绝对路径。
- 包名。
- URL。
2.3 模块依赖
模块依赖描述“当前模块需要哪些其他模块”。
清晰依赖的好处:
- 更容易理解代码关系。
- 更容易打包和摇树优化。
- 更容易发现无用依赖。
- 更容易拆分和测试。
2.4 模块加载
模块加载通常包含:
- 解析模块标识符。
- 下载或读取模块文件。
- 解析依赖。
- 执行模块代码。
- 缓存模块结果。
不同模块系统的加载时机不同:
- CommonJS:运行时同步加载。
- AMD:浏览器环境异步加载。
- ES6 模块:静态声明,浏览器异步加载和执行。
2.5 入口
入口模块是应用启动的起点。构建工具会从入口开始追踪依赖图。
// main.js
import { createApp } from "./app.js";
createApp();
2.6 异步依赖与动态依赖
异步依赖可以按需加载代码:
const module = await import("./dialog.js");
module.openDialog();
动态导入适合:
- 路由懒加载。
- 大型功能按需加载。
- 管理首屏包体积。
2.7 静态分析
ES6 模块的 import 和 export 是静态语法,必须位于模块顶层。这让引擎和构建工具可以在执行前分析依赖关系。
静态分析带来的能力:
- 提前发现导入导出错误。
- Tree shaking。
- 更好的循环依赖处理。
- 更高效的加载计划。
2.8 循环依赖
循环依赖是模块 A 依赖 B,B 又依赖 A。
// a.js
import { b } from "./b.js";
export const a = "a";
// b.js
import { a } from "./a.js";
export const b = "b";
循环依赖不一定错误,但容易导致初始化时机问题。实践中应尽量避免复杂循环,可以通过提取公共模块、依赖倒置等方式拆开。
3. 凑合的模块系统
在正式模块标准出现前,开发者常用立即调用函数表达式创建私有作用域。
const counter = (function () {
let value = 0;
return {
increment() {
value += 1;
},
getValue() {
return value;
},
};
})();
优点:
- 避免直接污染全局变量。
- 可以模拟私有变量。
缺点:
- 依赖关系不清晰。
- 加载顺序仍需手动控制。
- 无法被工具可靠静态分析。
4. ES6 之前的模块加载器
4.1 CommonJS
CommonJS 主要用于 Node.js。
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
const { add } = require("./math");
特点:
- 使用
require()导入。 - 使用
module.exports或exports导出。 - 同步加载。
- 运行时确定依赖。
- 模块会被缓存。
适合服务端文件系统环境,不天然适合浏览器同步网络加载。
4.2 AMD
AMD 主要面向浏览器异步加载。
define(["dep"], function (dep) {
return {
run() {
dep.doSomething();
},
};
});
特点:
- 异步加载依赖。
- 适合早期浏览器环境。
- 语法相对繁琐。
4.3 UMD
UMD 试图同时兼容 CommonJS、AMD 和全局变量方式。
适合库作者在多环境分发同一份代码。缺点是模板代码复杂,现代项目通常由构建工具处理兼容输出。
4.4 模块加载器的没落
随着 ES6 模块成为标准,浏览器、Node.js 和构建工具都逐渐围绕 ESM 建立生态。CommonJS 等方案仍存在,但新项目更推荐优先使用 ES 模块。
5. ES6 模块
5.1 模块标签及定义
浏览器中通过 type="module" 使用模块:
<script type="module" src="./main.js"></script>
模块脚本特点:
- 默认启用严格模式。
- 拥有独立作用域。
- 默认延迟执行,类似
defer。 - 支持
import和export。
5.2 模块加载
ES 模块会先构建依赖图,再按依赖顺序执行。浏览器会异步下载模块及其依赖,不阻塞 HTML 解析。
注意:
- 模块只执行一次。
- 同一个模块被多处导入时共享同一份实例。
- 顶层
this是undefined。
5.3 导出
命名导出:
export const name = "Alice";
export function sayHi() {
console.log("hi");
}
统一导出:
const name = "Alice";
function sayHi() {}
export { name, sayHi };
重命名导出:
export { sayHi as greet };
默认导出:
export default function createApp() {}
每个模块只能有一个默认导出。
5.4 导入
命名导入:
import { name, sayHi } from "./user.js";
重命名导入:
import { sayHi as greet } from "./user.js";
命名空间导入:
import * as user from "./user.js";
默认导入:
import createApp from "./app.js";
混合导入:
import createApp, { version } from "./app.js";
5.5 转移导出
模块可以重新导出其他模块的内容,常用于统一出口文件。
export { add } from "./math.js";
export { formatDate } from "./date.js";
也可以全部转移:
export * from "./constants.js";
常见目录结构:
components/
Button.js
Dialog.js
index.js
// index.js
export { default as Button } from "./Button.js";
export { default as Dialog } from "./Dialog.js";
5.6 动态导入
import() 返回 Promise:
button.addEventListener("click", async () => {
const { openDialog } = await import("./dialog.js");
openDialog();
});
适合代码分割和懒加载。
5.7 工作者模块
Worker 也可以使用模块形式:
const worker = new Worker("./worker.js", {
type: "module",
});
这样 worker 内部也能使用 import。
5.8 向后兼容
可通过 nomodule 为不支持模块的浏览器提供降级脚本:
<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>
6. CommonJS 与 ES Module 对比
| 对比项 | CommonJS | ES Module |
|---|---|---|
| 导入语法 | require() |
import |
| 导出语法 | module.exports |
export |
| 加载时机 | 运行时 | 静态解析 |
| 加载方式 | 通常同步 | 支持异步 |
| 适用环境 | Node.js 传统生态 | 浏览器与现代 Node.js |
| Tree shaking | 较困难 | 更友好 |
| 绑定关系 | 值拷贝倾向 | 实时绑定 |
7. 面试高频问题
7.1 为什么需要模块化?
模块化可以隔离作用域、声明依赖、复用代码、降低全局污染,让大型项目更容易维护和构建。
7.2 ES Module 为什么有利于 Tree shaking?
因为 ESM 的导入导出是静态语法,构建工具能在执行前分析哪些导出被使用,从而移除未使用代码。
7.3 import 和 require 有什么区别?
import 是 ESM 的静态语法,通常位于顶层,支持静态分析。require() 是 CommonJS 的运行时函数,可以在条件语句中调用,但不利于静态分析。
7.4 默认导出和命名导出怎么选?
一个模块只有一个主要能力时,可以用默认导出。一个模块提供多个明确能力时,命名导出更清晰,也更利于重构和自动导入。
7.5 动态 import 有什么用?
动态 import() 可以按需加载模块,常用于路由懒加载、弹窗、图表、编辑器等非首屏功能,能减少初始加载体积。
8. 复习清单
9. 一句话总结
第 26 章的核心是 JavaScript 从“全局脚本拼接”走向“标准模块系统”的过程;现代开发应优先理解和使用 ES Module,因为它让依赖关系、加载过程和构建优化都变得更可靠。

浙公网安备 33010602011771号