跳到主要内容

JavaScript 模块化

问题

JavaScript 有哪些模块化方案?ESM 和 CommonJS 有什么区别?

答案

JavaScript 模块化经历了从无模块IIFEAMDCommonJS,最终发展到原生 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

特性ESMCommonJS
语法import/exportrequire/module.exports
加载时机编译时(静态)运行时(动态)
值类型引用拷贝
顶层 thisundefined当前模块
异步支持✅ 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浏览器同步最原始
CommonJSNode.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 的主要区别?

答案

特性ESMCommonJS
语法import/exportrequire/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: 如何处理循环依赖?

答案

  1. 重构代码:将共享部分提取到第三个模块
  2. 延迟访问:在函数内部访问而非模块顶层
  3. 使用接口:依赖抽象而非具体实现
// 解决方案:延迟访问
// 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() 的区别?

答案

特性importimport()
类型声明表达式
位置模块顶层任意位置
时机编译时运行时
返回绑定Promise
动态路径
// 静态导入
import { foo } from './module';

// 动态导入
const modulePath = './module-' + name;
const module = await import(modulePath);

相关链接