JavaScript 模块化

当项目代码越来越多,把所有内容写在一个文件中是不可维护的。模块化让你将代码拆分成独立的文件,每个文件负责一个功能区域,通过 importexport 互相引用。

ES Module 基础

export — 导出

javascript
// math.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export class Calculator {
  multiply(a, b) {
    return a * b;
  }
}

import — 导入

javascript
// app.js
import { PI, add, Calculator } from './math.js';

console.log(PI);           // 3.14159
console.log(add(2, 3));    // 5
console.log(new Calculator().multiply(4, 5)); // 20

每个模块有自己的作用域——模块内部的变量不会泄漏到全局,也不会和其他模块冲突。

导出方式

命名导出(Named Export)

一个模块可以有多个命名导出:

javascript
// user.js
export const name = '张三';
export function greet() {
  return `你好,${name}`;
}
javascript
// 导入
import { name, greet } from './user.js';

// 导入时重命名
import { name as userName, greet as sayHello } from './user.js';

// 导入全部并放命名空间中
import * as User from './user.js';
console.log(User.name); // "张三"

默认导出(Default Export)

每个模块可以有一个默认导出:

javascript
// api.js
export default function fetchData() {
  return fetch('/api/data').then(res => res.json());
}
javascript
// 导入默认导出——可以任意命名
import getData from './api.js';

// 同时导入默认和命名导出
import getData, { baseUrl } from './api.js';

选择建议:主要导出一个功能时用默认导出(如一个类、一个组件),导出多个工具函数时用命名导出。一个项目中保持一致即可,混用是常见的但可行。

动态导入 import()

import() 返回一个 Promise,在运行时按需加载模块:

javascript
// 条件加载
if (user.isAdmin) {
  const adminModule = await import('./admin.js');
  adminModule.initAdminPanel();
}

// 点击时懒加载
button.addEventListener('click', async () => {
  const { heavyLibrary } = await import('./heavy-lib.js');
  heavyLibrary.process();
});

动态导入的优势:按需加载代码,减小初始包体积,加速首屏加载。这是现代前端工程中代码分割的基础。

HTML 中使用模块

html
<!-- type="module" 启用 ES Module -->
<script type="module" src="app.js"></script>

<!-- 内联模块 -->
<script type="module">
  import { greet } from './utils.js';
  console.log(greet('张三'));
</script>

type="module"<script>自动延迟执行(等同于 defer),且严格模式下运行。

模块 vs 传统脚本

特性<script><script type="module">
执行时机立即执行自动 defer
作用域全局模块作用域(隔离)
严格模式需要声明 'use strict'默认严格模式
import/export不可用可用
thiswindowundefined
多次引用多次执行单次执行(缓存)

模块组织建议

plaintext
js/
├── main.js          # 入口文件
├── utils/
│   ├── format.js    # 格式化工具
│   └── validate.js  # 验证工具
├── components/
│   ├── modal.js     # 模态框组件
│   └── carousel.js  # 轮播组件
└── api/
    └── index.js     # API 请求封装

一个模块只做一件事,文件名反映其功能。这是保持代码可维护的基本准则。

跨模块共享状态

模块是单例的——导出的同一个变量在所有导入的模块中共享:

javascript
// store.js
export const state = { count: 0 };
export function increment() {
  state.count++;
}
javascript
// moduleA.js
import { state, increment } from './store.js';
increment();
console.log(state.count); // 1
javascript
// moduleB.js
import { state } from './store.js';
console.log(state.count); // 1(与 moduleA 共享同一个 state 对象)

虽然模块单例可以共享状态,但直接导出一个可变对象会让数据流向难以追踪。在实际项目中,建议使用更明确的状态管理模式(如发布/订阅、Redux、或框架自带的状态管理)。