JavaScript 模块化
问题
JavaScript 有哪些模块化方案?ESM 和 CommonJS 有什么区别?
答案
JavaScript 模块化经历了从无模块到 IIFE、AMD、CommonJS,最终发展到原生 ESM 的演进过程。
模块化演进
IIFE 模式(早期)
// 立即执行函数表达式
const myModule = (function() {
// 私有变量
let privateVar = 0;
// 私有函数
function privateMethod(): void {
privateVar++;
}
// 公开 API
return {
increment(): void {
privateMethod();
},
getValue(): number {
return privateVar;
}
};
})();
myModule.increment();
console.log(myModule.getValue()); // 1
CommonJS
Node.js 采用的模块规范,同步加载模块。
基本用法
// math.js - 导出
const PI = 3.14159;
function add(a: number, b: number): number {
return a + b;
}
function multiply(a: number, b: number): number {
return a * b;
}
module.exports = {
PI,
add,
multiply
};
// 或者逐个导出
exports.PI = PI;
exports.add = add;
// app.js - 导入
const math = require('./math');
console.log(math.PI); // 3.14159
console.log(math.add(1, 2)); // 3
// 解构导入
const { add, multiply } = require('./math');
特点
// 1. 运行时加载(动态)
if (condition) {
const module = require('./moduleA');
}
// 2. 值的拷贝(非引用)
// counter.js
let count = 0;
module.exports = {
count,
increment(): void { count++; }
};
// app.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 - 仍是 0,因为是值的拷贝
// 3. 缓存机制
const a = require('./module');
const b = require('./module');
console.log(a === b); // true - 同一个实例
// 清除缓存
delete require.cache[require.resolve('./module')];
ESM (ES Modules)
ES6 引入的原生模块系统,静态分析。
基本用法
// math.ts - 导出
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// 默认导出
export default class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
// app.ts - 导入
// 命名导入
import { PI, add, multiply } from './math';
// 默认导入
import Calculator from './math';
// 混合导入
import Calculator, { PI, add } from './math';
// 全部导入
import * as math from './math';
console.log(math.PI);
console.log(math.add(1, 2));
// 重命名
import { add as addNumbers } from './math';
动态导入
// 条件导入
async function loadModule(): Promise<void> {
if (condition) {
const module = await import('./moduleA');
module.doSomething();
}
}
// 路由懒加载(React)
const LazyComponent = React.lazy(() => import('./Component'));
// 按需加载
button.onclick = async () => {
const { processData } = await import('./processor');
processData();
};
导出模式
// 1. 命名导出
export const name = 'Alice';
export function greet(): void {}
export class User {}
// 2. 默认导出
export default function(): void {}
export default class {}
export default { name: 'config' };
// 3. 聚合导出(re-export)
export { foo, bar } from './module';
export { default } from './module';
export { foo as myFoo } from './module';
export * from './module';
// 4. 混合导出
export { default as Component, Props } from './Component';
ESM vs CommonJS
| 特性 | ESM | CommonJS |
|---|---|---|
| 语法 | import/export | require/module.exports |
| 加载时机 | 编译时(静态) | 运行时(动态) |
| 值类型 | 引用 | 拷贝 |
| 顶层 this | undefined | 当前模块 |
| 异步支持 | ✅ import() | ❌ |
| Tree Shaking | ✅ | ❌ 困难 |
| 循环依赖 | 不同处理 | 可能问题 |
值引用 vs 值拷贝
// ESM - 值的引用
// counter.ts
export let count = 0;
export function increment(): void {
count++;
}
// app.ts
import { count, increment } from './counter';
console.log(count); // 0
increment();
console.log(count); // 1 - 值更新了
// --------------------------------
// CommonJS - 值的拷贝
// counter.js
let count = 0;
module.exports = {
count,
increment(): void { count++; },
getCount(): number { return count; }
};
// app.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 - 仍是 0
console.log(counter.getCount()); // 1 - 需要用函数获取
循环依赖
// ESM 循环依赖
// a.ts
import { b } from './b';
export const a = 'A';
console.log('a.ts:', b);
// b.ts
import { a } from './a';
export const b = 'B';
console.log('b.ts:', a);
// 执行 a.ts 输出:
// b.ts: undefined (a 还未初始化)
// a.ts: B
// ---------------------------------
// CommonJS 循环依赖
// a.js
const b = require('./b');
exports.a = 'A';
console.log('a.js:', b);
// b.js
const a = require('./a');
exports.b = 'B';
console.log('b.js:', a);
// 执行 a.js 输出:
// b.js: {} (a 的不完整导出)
// a.js: { b: 'B' }
AMD
异步模块定义,主要用于浏览器。
// 定义模块
define('math', [], function() {
return {
add(a: number, b: number): number {
return a + b;
}
};
});
// 使用模块
require(['math'], function(math) {
console.log(math.add(1, 2));
});
// 带依赖的模块
define('calculator', ['math'], function(math) {
return {
calculate(a: number, b: number): number {
return math.add(a, b);
}
};
});
UMD
通用模块定义,兼容多种环境。
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependency'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('dependency'));
} else {
// 浏览器全局变量
root.myModule = factory(root.dependency);
}
})(typeof self !== 'undefined' ? self : this, function(dependency) {
// 模块代码
return {
doSomething(): void {}
};
});
Tree Shaking
ESM 的静态分析特性支持 Tree Shaking(摇树优化)。
// utils.ts
export function used(): void {
console.log('used');
}
export function unused(): void {
console.log('unused'); // 打包时会被移除
}
// app.ts
import { used } from './utils';
used();
// unused 函数不会被打包
// ⚠️ 副作用会阻止 Tree Shaking
// utils.ts
console.log('side effect'); // 即使不导入也会执行
export function fn(): void {}
// 标记无副作用
// package.json
{
"sideEffects": false,
// 或指定有副作用的文件
"sideEffects": ["*.css", "*.scss"]
}
模块方案对比
| 方案 | 环境 | 加载 | 依赖 | 特点 |
|---|---|---|---|---|
| IIFE | 浏览器 | 同步 | 无 | 最原始 |
| CommonJS | Node.js | 同步 | 运行时 | 简单 |
| AMD | 浏览器 | 异步 | 运行时 | 复杂 |
| UMD | 通用 | 视环境 | 运行时 | 兼容性好 |
| ESM | 通用 | 异步 | 编译时 | 原生支持 |
实际应用
Node.js 中使用 ESM
// package.json
{
"type": "module" // 启用 ESM
}
// 或使用 .mjs 扩展名
// utils.mjs
export function add(a: number, b: number): number {
return a + b;
}
// 在 ESM 中使用 CommonJS
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const lodash = require('lodash');
打包工具配置
// webpack.config.js - 优化 Tree Shaking
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: true,
minimize: true
}
};
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
// 代码分割
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios']
}
}
}
}
};
常见面试问题
Q1: ESM 和 CommonJS 的主要区别?
答案:
| 特性 | ESM | CommonJS |
|---|---|---|
| 语法 | import/export | require/module.exports |
| 加载 | 编译时静态分析 | 运行时动态加载 |
| 值 | 引用绑定 | 值拷贝 |
| 异步 | 支持 import() | 不支持 |
| Tree Shaking | 支持 | 不支持 |
| 顶层 await | 支持 | 不支持 |
Q2: 什么是 Tree Shaking?
答案:
Tree Shaking 是消除未使用代码的优化技术。基于 ESM 的静态分析,打包工具可以确定哪些导出未被使用,从而在打包时移除。
条件:
- 使用 ESM 模块语法
- 避免副作用或正确标记
- 生产模式打包
Q3: 为什么 ESM 可以 Tree Shaking 而 CommonJS 不行?
答案:
ESM 是静态的,import/export 必须在顶层,编译时就能确定依赖关系。
CommonJS 是动态的,require 可以在任意位置,依赖关系要运行时才能确定。
// CommonJS - 运行时才知道导入什么
if (condition) {
module.exports.a = require('./a');
} else {
module.exports.b = require('./b');
}
// ESM - 编译时就确定
import { a } from './a';
import { b } from './b';
Q4: 如何处理循环依赖?
答案:
- 重构代码:将共享部分提取到第三个模块
- 延迟访问:在函数内部访问而非模块顶层
- 使用接口:依赖抽象而非具体实现
// 解决方案:延迟访问
// a.ts
import { getB } from './b';
export const a = 'A';
export function useB(): string {
return getB(); // 函数调用时 b 已初始化
}
// b.ts
import { a } from './a';
export function getB(): string {
return 'B' + a;
}
Q5: import 和 import() 的区别?
答案:
| 特性 | import | import() |
|---|---|---|
| 类型 | 声明 | 表达式 |
| 位置 | 模块顶层 | 任意位置 |
| 时机 | 编译时 | 运行时 |
| 返回 | 绑定 | Promise |
| 动态路径 | ❌ | ✅ |
// 静态导入
import { foo } from './module';
// 动态导入
const modulePath = './module-' + name;
const module = await import(modulePath);