模块化
了解 JavaScript 各模块规范的原理、语法与适用场景
JavaScript 模块化让你将代码拆分为独立文件,通过导入/导出机制在文件间共享功能。不同的运行环境和历史背景催生了多种模块规范,了解它们的区别有助于在不同场景下做出正确选择。
一、CommonJS(CJS)
CommonJS 是 Node.js 的原生模块系统,使用同步的 require() 加载模块,文件之间通过 module.exports 共享内容。
// math.js — 导出
const add = (a, b) => a + b
const multiply = (a, b) => a * b
module.exports = { add, multiply }// index.js — 导入
const { add, multiply } = require('./math')
console.log(add(2, 3)) // 5
console.log(multiply(2, 3)) // 6特点:
- 同步加载,适合服务端(磁盘 I/O 速度快)
module.exports导出的是值的拷贝(对象类型为引用拷贝)- 每个文件拥有独立的模块作用域,不污染全局
二、ES Modules(ESM)
ES Modules 是 ECMAScript 2015 引入的官方标准,现代浏览器和 Node.js v12+ 原生支持。新项目优先使用这种格式。
// math.js — 具名导出 + 默认导出
export const add = (a, b) => a + b
export default function greet(name) {
return `Hello, ${name}`
}// index.js — 导入
import greet, { add } from './math.js'
console.log(add(1, 2)) // 3
console.log(greet('World')) // Hello, WorldESM 也支持动态导入,在需要时按需加载模块:
// 点击时才加载模块(代码分割)
button.addEventListener('click', async () => {
const { heavyUtil } = await import('./heavy-util.js')
heavyUtil.run()
})特点:
- 静态分析结构,打包工具可做 Tree Shaking(移除未用代码)
import绑定的是实时引用(live binding),不是值的拷贝- 在 Node.js 中需将文件命名为
.mjs,或在package.json中设置"type": "module"
三、AMD
AMD(Asynchronous Module Definition)专为浏览器设计,依赖 RequireJS 等加载器实现异步加载,避免阻塞页面渲染。
// 定义模块(依赖 jQuery)
define(['jquery'], function ($) {
function showMessage(msg) {
$('body').append(`<p>${msg}</p>`)
}
return { showMessage }
})// 使用模块
require(['myModule'], function (myModule) {
myModule.showMessage('Hello, AMD')
})特点:
- 语法比 ESM 冗长,现已基本被 ESM 取代
- 需要引入额外的加载器库(如 RequireJS)
- 仍可在需要支持旧浏览器的遗留项目中看到
四、IIFE
IIFE(Immediately Invoked Function Expression,立即调用函数表达式)把模块代码包裹在一个立即执行的函数中,通过闭包隔离私有变量,避免污染全局作用域。
const MyLib = (function () {
// _version 是私有变量,外部无法访问
const _version = '1.0.0'
function greet(name) {
return `Hello, ${name} (v${_version})`
}
return { greet }
})()
console.log(MyLib.greet('World')) // Hello, World (v1.0.0)
console.log(MyLib._version) // undefined特点:
- 无需模块加载器,直接在
<script>标签中使用 - 适合打包为单文件分发的库(如早期的 jQuery)
- 多个 IIFE 之间若需共享状态,只能通过全局变量,容易出错
五、UMD
UMD(Universal Module Definition,通用模块定义)在运行时检测当前环境,自动适配 AMD、CommonJS 或全局变量三种方式。打包工具(Rollup、Webpack)在输出 UMD 格式时会自动生成这段模板代码。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dep'], factory) // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('dep')) // CommonJS
} else {
root.MyLib = factory(root.dep) // 全局变量
}
})(typeof self !== 'undefined' ? self : this, function (dep) {
return {
greet: (name) => `Hello, ${name}`
}
})特点:
- 同时兼容浏览器和 Node.js,适合需要广泛兼容的开源库
- 模板代码冗长,通常由打包工具自动生成,手写容易出错
- 现代项目建议直接发布 ESM,需要兼容旧环境时再附加 UMD 输出
六、SystemJS
SystemJS 是一个动态模块加载器,支持 System.register 格式。在浏览器原生 ESM 普及之前,它是运行 ESM 语法的主要方案,常见于早期 Angular 项目。
System.register(['./dep.js'], function (_export, _context) {
let dep
return {
setters: [function (d) { dep = d.default }],
execute: function () {
_export('greet', function (name) {
return `Hello, ${name}`
})
}
}
})特点:
- 支持动态导入和 URL 导入
- 现代项目中已较少使用,原生 ESM 可满足大多数场景
七、格式对比
各格式在环境兼容性和加载方式上各有侧重,下表帮助你快速定位适合的格式。
| 格式 | 运行环境 | 加载方式 | 适用场景 |
|---|---|---|---|
| CJS | Node.js | 同步 | 服务端应用、旧版 Node.js 包 |
| ESM | 浏览器 / Node.js | 异步 | 现代前端与全栈项目(推荐) |
| AMD | 浏览器 | 异步 | 遗留项目(RequireJS) |
| IIFE | 浏览器 | 同步 | 单文件库分发 |
| UMD | 通用 | 同/异步 | 需要兼容多环境的开源库 |
| SystemJS | 浏览器 | 异步 | 旧版 Angular 等遗留项目 |
如何选择? 新项目优先使用 ESM。发布 npm 包时,用打包工具同时输出 ESM 和 CJS 格式以兼容不同消费者。需要支持旧浏览器时,选择 UMD 或 IIFE。