{T}

模块化

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
// 两个文件共享同一个 counter

Q5: 如何导入 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,推荐在所有新项目中使用。对于大型应用,结合动态导入可实现更好的性能优化。