模块化
ES6 模块化是 JavaScript 官方的模块系统,使用 import 和 export 语法。
一、概述
1.1 什么是模块化
模块化是一种将复杂程序拆分为独立、可复用代码单元的程序设计方法。ES6 模块化(ES Module,简称 ESM)是 JavaScript 的官方标准模块系统,于 2015 年随 ES6 标准发布。
1.2 模块化的优势
code
┌─────────────────────────────────────────────────────────┐
│ 模块化核心优势 │
├─────────────────────────────────────────────────────────┤
│ ✦ 代码封装:隐藏内部实现,暴露公共接口 │
│ ✦ 命名隔离:避免全局命名冲突 │
│ ✦ 依赖管理:显式声明模块依赖关系 │
│ ✦ 代码复用:模块可在不同项目中复用 │
│ ✦ 按需加载:支持动态导入,优化性能 │
│ ✦ 静态分析:编译时确定依赖,支持 Tree-shaking │
└─────────────────────────────────────────────────────────┘1.3 模块化发展历程
code
无模块化 → IIFE/命名空间 → CommonJS/AMD → ES Module
↓ ↓ ↓ ↓
全局变量 作用域隔离 社区标准 官方标准二、模块系统架构
2.1 模块生命周期
code
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 解析 │ → │ 实例化 │ → │ 求值 │ → │ 完成 │
│ (Parse) │ │(Instantiate)│ │(Evaluate)│ │(Complete)│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
↓ ↓ ↓ ↓
解析模块 创建模块 执行代码 模块就绪
建立依赖 实例对象 绑定值 生命周期说明:
| 阶段 | 说明 | 特点 |
|---|---|---|
| 解析 | 读取模块源码,解析 import/export 语句 | 静态分析,发现错误 |
| 实例化 | 在内存中创建模块实例,建立绑定关系 | 不执行代码 |
| 求值 | 按依赖顺序执行模块代码 | 单次执行,结果缓存 |
2.2 模块作用域
javascript
// 模块内部具有独立作用域
// 顶层 this 为 undefined
// 顶层变量不会污染全局
console.log(this); // undefined(非严格模式下也是)
const privateVar = '私有变量'; // 外部无法访问
export const publicVar = '公共变量'; // 外部可访问三、导出(export)
3.1 命名导出
命名导出允许一个模块导出多个标识符,导入时必须使用相同的名称。
javascript
// ========== 方式一:声明时直接导出 ==========
export const PI = 3.14159;
export const E = 2.71828;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// ... 中间省略 ...
// ========== 方式四:导出计算属性 ==========
const a = 1;
const b = 2;
export { a + b as sum }; // ❌ 错误:导出必须是标识符
export const sum = a + b; // ✅ 正确3.2 默认导出
每个模块最多有一个默认导出,导入时可以自由命名。
javascript
// ========== 导出函数 ==========
export default function() {
console.log('默认导出的函数');
}
// 带名称的默认导出(名称仅用于调试)
export default function main() {
console.log('主函数');
}
// ========== 导出类 ==========
export default class {
// ... 中间省略 ...
}
export const version = '1.0.0';
export function helper() {
console.log('辅助函数');
}3.3 聚合导出
将多个模块的内容聚合后统一导出。
javascript
// ========== 转发导出 ==========
// utils.js 会重新导出 module1.js 和 module2.js 的内容
// 导出 module1.js 的指定成员
export { name, greet } from './module1.js';
// 重命名后导出
export { name as userName } from './module1.js';
// 导出模块的所有命名导出
export * from './module1.js';
// 整体导出为命名空间
export * as module1 from './module1.js';
// 导入后重新导出
import { add, subtract } from './math.js';
export { add, subtract };
// ========== 实际应用:统一入口文件 ==========
// index.js - 模块统一入口
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Select } from './Select.js';
export * from './common.js';3.4 导出对比表
| 特性 | 命名导出 | 默认导出 |
|---|---|---|
| 数量 | 多个 | 最多一个 |
| 导入时命名 | 必须使用原名 | 可自由命名 |
| 语法 | export { name } | export default expr |
| Tree-shaking | 支持更好 | 可能受限 |
| 适用场景 | 工具函数库 | 组件/类/主功能 |
四、导入(import)
4.1 命名导入
javascript
// ========== 导入指定成员 ==========
import { add, subtract, PI } from './math.js';
console.log(add(1, 2)); // 3
console.log(PI); // 3.14159
// ========== 重命名导入 ==========
import { add as sum, subtract as minus } from './math.js';
console.log(sum(5, 3)); // 8
// ========== 导入所有成员(命名空间导入) ==========
import * as Math from './math.js';
console.log(Math.add(1, 2)); // 3
console.log(Math.PI); // 3.14159
// ========== 仅执行模块(不导入绑定) ==========
import './polyfill.js'; // 执行模块代码,不导入任何内容
import './styles.css'; // 导入样式文件(构建工具支持)4.2 默认导入
javascript
// ========== 导入默认导出 ==========
import myFunc from './main.js'; // 自由命名
import Calculator from './math.js'; // 自由命名
myFunc();
const calc = new Calculator();
// ========== 同时导入默认导出和命名导出 ==========
import main, { helper, version } from './main.js';
main();
helper();
console.log(version);
// ========== 导入默认导出和所有命名导出 ==========
import main, * as utils from './main.js';
main();
utils.helper();4.3 动态导入
动态导入返回 Promise,支持按需加载和条件加载。
javascript
// ========== 基本用法 ==========
import('./utils.js')
.then(module => {
module.doSomething();
})
.catch(err => {
console.error('模块加载失败:', err);
});
// ========== async/await 语法 ==========
async function loadModule() {
try {
// ... 中间省略 ...
new Modal().show();
});
// ========== 动态路径 ==========
const lang = navigator.language;
const messages = await import(`./locales/${lang}.js`);4.4 导入模块的只读特性
javascript
// math.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './math.js';
console.log(count); // 0
increment();
console.log(count); // 1(ES Module 是值的引用)
count = 10; // ❌ TypeError: Assignment to constant variable.
// 导入的绑定是只读的,不能直接修改
// 但可以通过导出的函数修改
increment();
console.log(count); // 2五、ES Module vs CommonJS
5.1 核心差异
code
┌─────────────────────────────────────────────────────────────┐
│ ES Module vs CommonJS │
├──────────────────┬────────────────────┬─────────────────────┤
│ 特性 │ ES Module │ CommonJS │
├──────────────────┼────────────────────┼─────────────────────┤
│ 语法 │ import/export │ require/module.exports │
├──────────────────┼────────────────────┼─────────────────────┤
│ 加载时机 │ 编译时静态分析 │ 运行时动态加载 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 输出方式 │ 值的引用(绑定) │ 值的拷贝 │
├──────────────────┼────────────────────┼─────────────────────┤
│ this 指向 │ undefined │ 当前模块对象 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 循环依赖 │ 动态引用 │ 输出已执行部分 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 模块加载 │ 异步 │ 同步 │
├──────────────────┼────────────────────┼─────────────────────┤
│ Tree-shaking │ 支持 │ 不支持 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 运行环境 │ 浏览器/Node.js │ Node.js │
└──────────────────┴────────────────────┴─────────────────────┘5.2 值引用 vs 值拷贝
javascript
// ========== ES Module(值的引用) ==========
// lib.js
export let counter = 0;
export function increment() {
counter++;
}
// main.js
import { counter, increment } from './lib.js';
console.log(counter); // 0
increment();
console.log(counter); // 1 ✅ 引用更新
// ========== CommonJS(值的拷贝) ==========
// lib.js
let counter = 0;
function increment() {
counter++;
}
module.exports = { counter, increment };
// main.js
const lib = require('./lib');
console.log(lib.counter); // 0
lib.increment();
console.log(lib.counter); // 0 ❌ 拷贝未更新5.3 加载机制对比
javascript
// ========== ES Module:编译时加载 ==========
// 静态分析,无法使用变量作为路径
import path from './utils.js'; // ✅ 静态路径
// import path from dynamicPath; // ❌ 不支持动态路径
// 必须使用动态导入
const modulePath = './utils.js';
const utils = await import(modulePath); // ✅ 动态导入
// ========== CommonJS:运行时加载 ==========
// 可以使用变量和表达式
const path = './utils.js';
const utils = require(path); // ✅ 动态路径
// 可以条件加载
if (condition) {
const module = require('./module.js');
}六、环境配置
6.1 浏览器环境
html
<!-- ========== 使用 ES Module ========== -->
<script type="module" src="./main.js"></script>
<!-- 内联模块 -->
<script type="module">
import { greet } from './utils.js';
greet();
</script>
<!-- ========== 兼容性处理 ========== -->
<!-- 新浏览器加载模块,旧浏览器加载普通脚本 -->
<script type="module" src="./main.js"></script>
<script nomodule src="./fallback.js"></script>浏览器加载特点:
| 特性 | 说明 |
|---|---|
| 跨域限制 | 需要正确的 CORS 头 |
| CORS 检查 | 本地文件需使用服务器 |
| 严格模式 | 自动启用严格模式 |
| 延迟执行 | 等同于 defer 属性 |
| 执行顺序 | 按依赖顺序执行 |
6.2 Node.js 环境
javascript
// ========== 方式一:使用 .mjs 扩展名 ==========
// 文件名:main.mjs
import { readFile } from 'fs/promises';
// ========== 方式二:package.json 配置 ==========
// package.json
{
"type": "module",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}
// ========== 方式三:命令行参数 ==========
node --experimental-modules main.js
// ========== 内置模块导入 ==========
// Node.js 内置模块支持命名导出
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
// ========== 导入 CommonJS 模块 ==========
// CommonJS 模块的 module.exports 作为默认导出
import _ from 'lodash'; // 默认导入
import lodash from 'lodash'; // 等同于上面6.3 导入路径规则
javascript
// ========== 相对路径 ==========
import { foo } from './module.js'; // 当前目录
import { bar } from '../parent.js'; // 上级目录
import { baz } from '../../root.js'; // 上上级目录
// ========== 绝对路径 ==========
import { config } from '/absolute/path/to/module.js';
// ========== 包路径(node_modules) ==========
import _ from 'lodash';
import { Button } from 'antd/lib/button';
// ========== URL 路径(浏览器) ==========
import React from 'https://cdn.skypack.dev/react';
// ========== Node.js 子路径导入 ==========
// 配合 package.json exports 字段
import { utils } from 'my-package/utils';七、循环依赖处理
7.1 循环依赖示例
javascript
// ========== 场景:模块 A 和 B 相互依赖 ==========
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('a.js:', b); // 'b.js: undefined'(b.js 还未执行完)
// b.js
import { a } from './a.js';
export const b = 'b';
console.log('b.js:', a); // undefined(a.js 还未执行完)7.2 解决方案
javascript
// ========== 方案一:延迟访问 ==========
// a.js
import { b, getB } from './b.js';
export const a = 'a';
export function getA() {
return a;
}
console.log('a.js:', getB()); // ✅ 'b'(函数执行时已初始化)
// b.js
import { a, getA } from './a.js';
export const b = 'b';
export function getB() {
return b;
}
console.log('b.js:', getA()); // ✅ 'a'
// ========== 方案二:重新组织代码结构 ==========
// 提取公共部分到独立模块
// common.js
export const shared = 'shared';
// a.js
import { shared } from './common.js';
export const a = 'a';
// b.js
import { shared } from './common.js';
export const b = 'b';八、最佳实践
8.1 导出建议
javascript
// ✅ 推荐:使用命名导出(利于 Tree-shaking)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// ✅ 推荐:统一在文件末尾导出
const a = 1;
const b = 2;
function add(x, y) { return x + y; }
export { a, b, add };
// ✅ 推荐:工具库使用命名导出
export const debounce = (fn, delay) => { /* ... */ };
export const throttle = (fn, delay) => { /* ... */ };
// ✅ 推荐:组件/类使用默认导出
export default class Button { /* ... */ }
// ❌ 避免:导出可变状态
export let state = {}; // 外部可直接修改
// ✅ 改进:提供访问器
let _state = {};
export function getState() { return _state; }
export function setState(newState) { _state = newState; }8.2 导入建议
javascript
// ✅ 推荐:明确导入需要的成员
import { add, subtract } from './math.js';
// ✅ 推荐:导入过多时使用命名空间
import * as Utils from './utils.js';
Utils.formatDate(new Date());
// ✅ 推荐:动态导入用于大型模块
async function loadChart() {
const { Chart } = await import('chart.js');
return new Chart(ctx, config);
}
// ❌ 避免:导入时重命名过多
import {
a as alpha,
b as beta,
c as gamma
} from './module.js';
// ❌ 避免:循环依赖
// 重新设计模块结构,避免循环依赖8.3 模块设计原则
code
┌─────────────────────────────────────────────────────────────┐
│ 模块设计原则 │
├─────────────────────────────────────────────────────────────┤
│ 1. 单一职责:每个模块只负责一个功能 │
│ 2. 高内聚:相关功能放在同一模块 │
│ 3. 低耦合:模块间依赖最小化 │
│ 4. 接口简洁:只导出必要的 API │
│ 5. 避免副作用:模块加载不应有副作用 │
│ 6. 文档清晰:提供清晰的使用说明 │
└─────────────────────────────────────────────────────────────┘九、完整示例
9.1 工具函数模块
javascript
// ========== utils/string.js ==========
/**
* 字符串工具函数模块
* @module utils/string
*/
/**
* 首字母大写
* @param {string} str - 输入字符串
* @returns {string} 转换后的字符串
*/
export function capitalize(str) {
// ... 中间省略 ...
// 默认导出对象形式
export default {
capitalize,
camelToKebab,
generateId
};9.2 配置模块
javascript
// ========== config/index.js ==========
/**
* 应用配置模块
* @module config
*/
// 环境配置
const environments = {
development: {
apiUrl: 'http://localhost:3000',
debug: true
},
// ... 中间省略 ...
// 默认导出完整配置
export default {
...environments[currentEnv],
env: currentEnv
};9.3 组件模块
javascript
// ========== components/Modal.js ==========
/**
* 模态框组件
* @module components/Modal
*/
// 导入依赖
import { createElement } from '../utils/dom.js';
import { EventEmitter } from '../utils/events.js';
// 私有变量(模块内部使用)
const MODAL_CLASS = 'modal';
// ... 中间省略 ...
}
// 导出辅助函数
export function createModal(options) {
return new Modal(options);
}9.4 应用入口
javascript
// ========== main.js ==========
/**
* 应用入口
* @module main
*/
// 导入模块
import { config, isDev } from './config/index.js';
import Modal, { createModal } from './components/Modal.js';
import * as StringUtils from './utils/string.js';
// 初始化应用
// ... 中间省略 ...
const formattedName = StringUtils.capitalize('hello world');
console.log(formattedName); // 'Hello world'
}
// 启动应用
init().catch(console.error);十、常见问题解答
Q1: 为什么导入时必须写完整路径?
javascript
// ❌ 错误:省略扩展名
import { foo } from './module';
// ✅ 正确:包含扩展名
import { foo } from './module.js';原因:ES Module 规范要求使用完整路径,确保模块定位的准确性和一致性。构建工具(如 Webpack、Vite)可以配置省略扩展名。
Q2: import 和 require 可以混用吗?
javascript
// ⚠️ 不推荐混用
import { foo } from './module.js';
const bar = require('./module.js');
// 建议统一使用 ES Module
import { foo, bar } from './module.js';说明:虽然技术上可以混用,但可能导致意料之外的问题。建议统一使用 ES Module。
Q3: 如何实现按需加载?
javascript
// ✅ 使用动态导入
document.getElementById('btn').addEventListener('click', async () => {
const { default: HeavyComponent } = await import('./HeavyComponent.js');
// 组件只在需要时加载
});
// ✅ 条件加载
if (featureFlags.advancedMode) {
const { AdvancedEditor } = await import('./AdvancedEditor.js');
}Q4: 导入的模块在哪里执行?
javascript
// 模块代码只执行一次,结果被缓存
import './module.js'; // 执行 module.js
import './module.js'; // 不再执行(缓存)
// 不同文件导入同一模块,共享同一实例
import { counter } from './counter.js'; // 文件 A
import { counter } from './counter.js'; // 文件 B
// 两个文件共享同一个 counterQ5: 如何导入 JSON 文件?
javascript
// ========== 静态导入(需要构建工具或 Node.js) ==========
import data from './data.json' assert { type: 'json' };
console.log(data);
// ========== 动态导入 ==========
const response = await fetch('./data.json');
const data = await response.json();Q6: 浏览器中如何处理裸模块说明符?
javascript
// ❌ 浏览器原生不支持裸模块说明符
import _ from 'lodash';
// ✅ 使用完整 URL
import _ from 'https://cdn.skypack.dev/lodash';
// ✅ 使用 importmap(现代浏览器)
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.skypack.dev/lodash"
}
}
</script>
// 然后可以使用
import _ from 'lodash';十一、注意事项
11.1 语法限制
javascript
// ❌ 不能在块级作用域中使用静态 import
if (condition) {
import { foo } from './module.js'; // SyntaxError
}
// ✅ 使用动态 import
if (condition) {
const { foo } = await import('./module.js');
}
// ❌ import 必须在顶层
function loadModule() {
import { foo } from './module.js'; // SyntaxError
}
// ✅ 动态 import 可以在任何位置
async function loadModule() {
const { foo } = await import('./module.js');
}11.2 严格模式
javascript
// ES Module 自动启用严格模式
export function example() {
// 'use strict' 自动应用
this; // undefined(非严格模式下可能是全局对象)
// 未声明变量赋值会报错
undeclaredVar = 1; // ReferenceError
}11.3 异步加载
javascript
// ES Module 加载是异步的
console.log('1');
import { foo } from './module.js'; // 模块代码在所有静态导入完成后执行
console.log('2');
// 输出顺序:1 → 2 → module.js 中的代码
// 实际上静态 import 会被提升到模块顶部执行十二、参考资源
💡 提示:ES Module 是现代 JavaScript 的标准模块系统,支持静态分析和 Tree-shaking,推荐在所有新项目中使用。对于大型应用,结合动态导入可实现更好的性能优化。