从出现背景、设计思想、导入导出方式和使用场景四个维度,对比分析 IIFE、CommonJS 和 ES6 Modules 的异同。
一、 IIFE(立即执行函数)
1、出现背景
- 时代:早期前端(2000~2010 年),无模块化标准。
- 问题:全局变量污染、脚本依赖顺序难以管理(如 jQuery 插件依赖 jQuery)。
2、设计思想
- 核心:通过函数作用域(闭包)隔离代码,返回一个对象暴露接口。
- 目标:减少全局污染,模拟模块的私有和公有成员。
3、导入导出方式
// 导出模块(moduleA.js)
var moduleA = (function() {
var privateVar = '内部数据';
return {
publicVar: '公开数据',
getPrivate: function() { return privateVar; }
};
})();
// 导入模块(依赖需手动管理)
(function(moduleA) {
console.log(moduleA.publicVar); // '公开数据'
})(moduleA);
特点:
- 导出:返回一个对象。
- 导入:通过全局变量或参数传递依赖。
4、使用场景
- 早期浏览器环境:如 jQuery 插件开发。
- 小型项目:无需复杂依赖管理的场景。
- 兼容性要求高:不支持现代模块化的旧系统。
缺点:
- 依赖需手动维护,无法动态加载。
- 无标准化规范,难以规模化协作。
二、 CommonJS(Node.js 模块化)
1、出现背景
- 时代:2009 年 Node.js 诞生,需服务端模块化方案。
- 问题:浏览器端无模块化标准,服务端需同步加载文件。
2、设计思想
- 核心:每个文件是一个模块,同步加载,通过
module.exports
导出、require
导入。 - 目标:简单高效的模块依赖管理,适合服务端 I/O 场景。
3、导入导出方式
// 导出模块(math.js)
module.exports = {
add: (a, b) => a + b,
PI: 3.14
};
// 导入模块(app.js)
const math = require('./math.js');
console.log(math.add(2, 3)); // 5
特点:
- 导出:
module.exports
或exports.xxx
。 - 导入:
require
动态加载(运行时解析)。 - 值拷贝:导出的是值的副本(原模块修改不影响已导入的值)。
4、使用场景
- Node.js 服务端:天然支持,无需额外工具。
- 传统前端打包:Webpack/Browserify 可将 CommonJS 转浏览器兼容代码。
- 同步加载场景:如配置文件读取。
缺点:
- 同步阻塞:不适合浏览器直接使用(需打包解决)。
- 动态加载:无法静态分析(不利于 Tree Shaking)。
三、 ES6 Modules(ESM)
1、出现背景
- 时代:2015 年 ES6 标准化,解决浏览器和服务端统一模块化问题。
- 问题:CommonJS 不适合浏览器,AMD/UMD 复杂且非原生。
2、设计思想
- 核心:静态化模块系统,通过
export
导出、import
导入。 - 目标:语言层面支持模块化,兼容浏览器和服务端。
3、导入导出方式
ES Modules(ESM)提供了多种灵活的导入导出方式,以下是所有可能的用法,涵盖命名导出/导入、默认导出/导入、混合导出、动态导入等场景。
1) 导出(Export)
- 命名导出(Named Exports)
每个模块可以导出多个命名变量/函数/类。
// 方式1:直接导出声明
export const name = 'Alice';
export function greet() { return 'Hello!'; }
export class Person { constructor(name) { this.name = name; } }
// 方式2:先定义后统一导出
const age = 30;
const city = 'Beijing';
export { age, city };
// 方式3:导出时重命名
export { age as userAge, city as userCity };
- 默认导出(Default Export)
每个模块只能有一个默认导出(通常用于导出主功能)。
// 方式1:直接导出默认值
export default function() { return 'Default Function'; }
// 方式2:先定义后默认导出
const foo = { key: 'value' };
export default foo;
// 方式3:默认导出类
export default class MyClass {}
- 混合导出(Named + Default)
export const version = '1.0';
export default function main() { return 'Main Function'; }
- 重新导出(Re-export)
从其他模块聚合导出(常用于封装多模块)。
// 重新导出其他模块的所有命名导出
export * from './moduleA.js';
// 重新导出其他模块的默认导出(需别名)
export { default as ModuleB } from './moduleB.js';
// 选择性重新导出
export { foo, bar as myBar } from './utils.js';
2) 导入(Import)
- 命名导入(Named Imports)
// 基本用法
import { name, greet, Person } from './module.js';
// 导入时重命名
import { name as userName, greet as sayHello } from './module.js';
// 导入全部命名导出(绑定到对象)
import * as module from './module.js';
console.log(module.name); // 访问导出的变量
- 默认导入(Default Import)
// 默认导入(名称可自定义)
import mainFunction from './module.js';
import MyClass from './module.js'; // 默认导出的类
- 混合导入(Named + Default)
import defaultExport, { namedExport1, namedExport2 } from './module.js';
import defaultExport, * as namedExports from './module.js';
- 动态导入(Dynamic Import)
按需异步加载模块(返回 Promise)。
// 方式1:then 链式调用
import('./module.js').then(module => {
console.log(module.default); // 默认导出
console.log(module.namedExport); // 命名导出
});
// 方式2:async/await
async function loadModule() {
const module = await import('./module.js');
// 使用模块...
}
- 副作用导入(仅执行模块,不导入内容)
import './init.js'; // 执行模块中的代码(如 polyfill 或初始化逻辑)
3) 特殊用法与注意事项
1. 导出值的绑定关系
- ESM 导出的是动态绑定(引用),修改原模块会影响导入的值。
// counter.js export let count = 0; export function increment() { count++; } // app.js import { count, increment } from './counter.js'; console.log(count); // 0 increment(); console.log(count); // 1(值同步更新)
2. 默认导出的本质
- 默认导出实际上是名为
default
的特殊命名导出。// 以下两种写法等价 export default function foo() {} export { foo as default };
3. 浏览器直接使用 ESM
<script type="module">
import { greet } from './module.js';
greet();
</script>
注意:
- 需通过 HTTP 协议运行(
file://
协议会报错)。 - 支持相对/绝对路径或完整的 URL(不支持省略扩展名)。
4) 完整示例
模块定义(math.js)
// 命名导出
export const PI = 3.14;
export function sum(a, b) { return a + b; }
// 默认导出
export default function multiply(a, b) { return a * b; }
模块使用(app.js)
// 导入默认导出和命名导出
import multiply, { PI, sum } from './math.js';
// 动态导入
const loadModule = async () => {
const math = await import('./math.js');
console.log(math.default(2, 3)); // 6
};
5)总结
分类 | 语法 | 用途 |
---|---|---|
命名导出 | export const/function/class... | 导出多个变量/函数/类 |
默认导出 | export default ... | 导出模块的主功能 |
混合导出 | export default + export {...} | 同时导出默认和命名内容 |
重新导出 | export * from 'module' | 聚合多个模块的导出 |
命名导入 | import { x, y } from 'module' | 导入特定命名导出 |
默认导入 | import x from 'module' | 导入默认导出 |
动态导入 | import('module') | 按需异步加载模块 |
6)使用场景
- 现代前端项目:React/Vue/Angular + Webpack/Rollup/Vite。
- 浏览器原生支持:
<script type="module">
直接使用。 - Node.js 新项目:需配置
"type": "module"
或.mjs
后缀。
优势:
- 静态分析:支持 Tree Shaking 优化。
- 标准化:未来唯一模块化标准。
兼容性问题:
- 旧浏览器需通过打包工具转译。
四、三类模块化对比总结
维度 | IIFE | CommonJS | ES Modules |
---|---|---|---|
出现背景 | 解决全局污染 | Node.js 服务端需求 | 浏览器/服务端统一化 |
设计思想 | 闭包隔离作用域 | 同步加载,文件即模块 | 静态化,语言级支持 |
导出方式 | 返回对象 | module.exports | export |
导入方式 | 全局变量/参数传递 | require | import |
加载时机 | 同步(脚本顺序) | 同步(运行时) | 静态(编译时) |
值传递 | 对象引用 | 值拷贝 | 动态绑定(引用) |
适用场景 | 旧浏览器/简单项目 | Node.js/传统前端打包 | 现代前端/Node.js |
五、演进趋势
- 淘汰 IIFE:仅用于兼容旧代码或库开发(如 UMD)。
- CommonJS 逐步迁移:Node.js 新项目推荐 ESM,旧项目逐步迁移。
- ES Modules 成为未来:浏览器原生支持,工具链全面适配。
选择建议:
- 新项目一律使用 ES Modules。
- 库开发提供 ESM + CommonJS 双版本。
- 旧项目按需迁移(如 Webpack 支持混合使用)。