
1.7 模块化体系
模块化是现代前端工程化的核心基石,通过将代码拆分为独立、可复用的模块,实现作用域隔离、依赖管理、按需加载与代码复用。
ES6 推出的 ES Modules(ESM)是 JavaScript 官方标准化的模块化方案,统一了早期 CommonJS、AMD 等多种社区规范,是当前前端开发的标准模块化方式,也是构建工具、框架生态的底层基础。
1.7.1 ES Modules:import /export 标准语法
ES Modules 基于 import 和 export 关键字实现模块的导入与导出,模块文件自动拥有独立作用域,只有显式导出的成员才能被外部访问。
一、export 导出语法
导出分为命名导出 和默认导出两种形式,支持单个导出、批量导出、重命名导出与聚合导出。
- 命名导出
- 一个模块可以有多个命名导出,导入时需与导出名严格对应。
javascript
// 单个声明时直接导出
export const baseURL = 'https://api.example.com';
export function formatDate(date) {
return date.toISOString().slice(0, 10);
}
export class User {
constructor(name) {
this.name = name;
}
}
// 声明后统一批量导出
const version = '1.0.0';
function log(msg) {
console.log(msg);
}
export { version, log };
- 重命名导出
- 通过 as 关键字修改导出的对外名称。
javascript
const innerFn = () => {};
export { innerFn as publicFn };
- 默认导出
- 一个模块只能有一个默认导出,对应一个默认值,适合导出单个核心成员(如单个类、单个函数)。
javascript
// 导出函数
export default function add(a, b) {
return a + b;
}
// 导出对象
export default {
name: 'utils',
version: '1.0.0'
};
- 聚合导出(中转导出)
- 在入口模块中统一转发其他模块的导出,简化外部引用路径。
javascript
// 转发所有命名导出
export * from './utils.js';
// 转发并重命名
export { default as DateUtil } from './date.js';
二、import 导入语法
对应导出语法,导入分为命名导入、默认导入、整体导入与重命名导入。
- 命名导入
- 使用花括号包裹,导入名必须与导出名完全一致。
javascript
import { baseURL, formatDate } from './config.js';
console.log(baseURL);
- 重命名导入
- 通过 as 修改导入后的变量名,解决命名冲突问题。
javascript
import { formatDate as format } from './config.js';
- 默认导入
- 无需花括号,可自定义导入名称。
javascript
import add from './add.js';
console.log(add(1, 2));
- 整体命名空间导入
- 通过 * as 将模块所有命名导出挂载到一个对象上。
javascript
import * as utils from './utils.js';
console.log(utils.version);
utils.log('test');
三、基础语法约束
- import / export 只能出现在模块顶层作用域,不能嵌套在函数、条件语句等块级作用域中
- 模块路径支持相对路径与绝对路径,省略后缀时需构建工具支持
- 导入的变量是只读引用,不可在导入侧直接修改导出值
1.7.2 动态 import () 按需加载
静态 import 语法必须在模块顶层执行,无法实现条件加载与按需加载。ES2020 引入的动态 import() 以函数形式调用,返回一个 Promise 对象,可在任意时机动态加载模块,是性能优化的核心手段。
一、基础语法
javascript
import(modulePath)
.then(module => {
// module 为模块导出对象
module.default();
})
.catch(err => {
console.error('模块加载失败:', err);
});
配合 async/await 使用更简洁:
javascript
async function loadModule() {
try {
const utils = await import('./utils.js');
utils.doSomething();
} catch (err) {
console.error('加载失败', err);
}
}
二、典型应用场景
- 条件加载:根据运行环境、特性支持度按需加载模块
javascript
if (!window.IntersectionObserver) {
await import('./intersection-polyfill.js');
}
- 交互触发加载:用户触发特定操作时再加载对应功能模块,降低首屏体积
javascript
// 点击按钮后再加载弹窗模块
modalBtn.addEventListener('click', async () => {
const Modal = (await import('./Modal.js')).default;
new Modal().show();
});
- 路由懒加载:单页应用中按路由拆分代码,进入对应页面时才加载页面资源
- 大依赖延迟加载:体积较大的第三方库(如图表、富文本编辑器)在使用前再加载
1.7.3 模块作用域与依赖机制
ES Modules 拥有独立的作用域与明确的依赖执行规则,从语法层面解决了全局变量污染、依赖顺序混乱等问题。
一、模块作用域
每个 ES Module 都拥有独立的私有作用域:
- 模块内声明的变量、函数、类默认仅在模块内部可见,不会泄露到全局作用域
- 只有通过 export 显式导出的成员,才能被外部模块访问
- 不同模块之间的同名变量不会互相冲突
对比传统脚本文件:多个普通 script 标签共享全局作用域,容易出现变量覆盖、命名冲突;模块文件天然隔离,无需手动封装闭包。
二、单例与引用机制
- 单例执行:同一个模块被多次导入时,仅会在第一次导入时执行一次模块代码,后续导入直接复用导出结果,不会重复执行
- 值的引用:导入的是导出值的引用,而非值的拷贝。若模块内部修改了导出的变量,所有导入该模块的地方都会同步更新
javascript
// counter.js
export let count = 0;
export function increment() {
count++;
}
// a.js
import { count, increment } from './counter.js';
increment();
console.log(count); // 1
// b.js
import { count } from './counter.js';
console.log(count); // 1,与 a.js 共享同一份引用
三、依赖执行规则
模块按照依赖树深度优先的顺序执行:
- 先执行当前模块的所有依赖模块,再执行当前模块自身代码
- 循环依赖场景下,已执行的部分会正常导出,未执行的部分为 undefined,需避免复杂循环依赖
四、默认严格模式
所有 ES Module 自动运行在严格模式下,无需手动声明 'use strict',因此模块内默认遵循严格模式的所有约束(如禁止未声明变量赋值、禁止 with 语句等)。