在没有 ES Module、打包器也不普及的阶段,很多 JavaScript 项目都会用 IIFE(立即执行函数表达式)完成最基础的模块化封装。它的思路并不复杂:把变量和函数限制在一个立即执行的私有作用域里,只把真正需要的能力通过返回对象暴露出去。理解这一点后,你不仅能读懂老项目里的模块写法,也能判断哪些 IIFE 代码已经具备迁移到现代模块系统的条件。
IIFE 模块化到底解决了什么问题
IIFE 的核心价值,不是“语法技巧”,而是用 JavaScript 自身的函数作用域,把一段逻辑封装成边界清晰的小模块。它不依赖构建工具,也不依赖新语法,纯靠语言机制就能完成隔离。
常见收益主要有三类:
- 把内部变量和函数私有化,避免外部随意访问或修改
- 减少全局作用域里的名字数量,降低命名冲突风险
- 通过参数显式传入依赖,方便适配不同运行环境
因此,IIFE 更像是早期 JavaScript 中实现“模块边界”的通用办法。它虽然不等同于今天的 ES Module,但两者在“输入、处理、输出分离”这一点上是相通的。
怎么封装私有状态,只暴露必要接口
用 IIFE 做模块化时,最重要的一步是把状态和实现细节留在函数内部。IIFE 执行完成后,外部能拿到的通常只有它 return 出去的对象;没被暴露的变量和函数,默认都无法直接访问。

这类结构最适合放置私有状态,例如:
let count = 0const config = {...}- 仅供内部调用的辅助函数
这些内容不会泄漏到全局作用域,因此也不会被外部代码直接覆盖。
封装时还有一个常见细节:不要轻易把原始状态直接挂到返回对象上。比如直接写 count: count,会让外部更容易绕开你的控制逻辑。更稳妥的方式通常是只提供读取方法或操作方法,例如 inc()、get(),把状态修改统一收口。
典型场景就是计数器模块。count 保留在 IIFE 内部,外部只能通过 module.inc() 增加计数,或通过 module.get() 读取结果。这样做的好处是,状态变化路径非常明确,后续即便要增加校验、日志或边界处理,也有固定入口可改。
为什么它能减少全局污染和命名冲突
早期前端脚本往往通过多个 script 标签直接加载。只要变量都写在全局作用域里,不同文件之间就很容易互相覆盖,尤其是像 utils、init 这样高频名字,冲突概率非常高。
IIFE 的作用就是给每一段逻辑单独划出一个独立作用域。即使多个模块内部都定义了同名变量或函数,它们也彼此隔离,不会互相干扰。
常见写法如下:
var slider = (function(){
// 内部实现
return {
// 对外接口
};
})();
这里的 slider 只是模块接口的引用,可以理解为外部访问这段逻辑的“门把手”,并不等于模块内部的全部实现细节。
这也是很多旧版 jQuery 插件喜欢采用 IIFE 的原因。插件代码通常被包裹在 IIFE 中,只依赖传入的 $ 参数,而不是到处直接污染 window。相比简单地在全局写下 var a = 1; var b = 2;,IIFE 让整块逻辑更接近真正的“自包含”。
IIFE 如何做依赖注入与环境适配
IIFE 不只是“包一层函数”这么简单。因为它可以接收参数,所以天然适合把外部依赖显式传进来,比如 window、document、jQuery,甚至某些运行环境标记。
常见形式是:
(function(global, doc, $) {
...
})(typeof window !== 'undefined' ? window : globalThis, document, jQuery);
这种写法有几个实际意义。
依赖来源更清楚
模块使用了什么,不再靠“默认存在的全局变量”去猜,而是通过参数明确写出来。阅读时更直观,测试时也更容易替换依赖。

更方便兼容不同环境
像 typeof window !== 'undefined' ? window : globalThis 这样的写法,本质上是在判断当前运行环境,再把合适的全局对象传入模块。这比在模块内部到处写环境判断更集中,也更容易维护。
兼容旧环境的细节处理
一些老代码会把 undefined 也声明成形参传入,目的是防止低版本浏览器里 undefined 被篡改,导致判断逻辑失效。这个写法今天已经不常见,但在阅读旧项目时仍然值得认识。
从工程角度看,显式依赖注入的最大价值是边界更清晰。模块不再偷偷依赖一堆全局对象,后续无论是测试、重构,还是迁移到 ES Module,成本都会更低。
它和 ES Module 之间是什么关系
虽然 IIFE 属于较早期的模块化方案,但它并不是和现代模块系统完全割裂的老写法。恰恰相反,很多结构良好的 IIFE,本身就已经具备了迁移基础。
原因在于,它已经把模块边界拆成了比较清楚的三部分:
- 输入:通过参数传入依赖
- 处理:在私有作用域中完成内部逻辑
- 输出:通过
return暴露对外接口
因此迁移到 ES Module 往往很自然,例如:
- 原来
return { init, destroy },可以改成export { init, destroy } - 原来
(function($) {...})(jQuery),可以改成import $ from 'jquery' - 原来依赖
script标签顺序保证加载关系,现在则通过import显式声明依赖
也就是说,IIFE 最大的历史价值,不只是“在旧环境里能用”,还在于它训练了开发者对模块边界的理解。一个 IIFE 如果已经做到接口收敛、依赖明确、内部状态不外泄,那么它通常也更容易被整理成现代模块。
现在还需要了解 IIFE 吗
如果你正在写新项目,主流选择当然还是 ES Module;但 IIFE 依然值得掌握,原因主要有两个。
第一,很多历史项目、插件代码和旧教程仍然大量使用它。不了解 IIFE,读这类代码时就很难快速分辨哪些是私有实现,哪些才是真正的公共接口。
第二,IIFE 背后的封装思想并没有过时。即使换成 ES Module、CommonJS 或其他模块系统,私有状态、显式依赖、最小暴露接口这些原则仍然成立。
所以可以把 IIFE 看成 JavaScript 模块化演进中的关键一环:它不再是新项目的首选,却仍然是理解旧代码、梳理封装边界和评估迁移路径时非常有用的基础工具。













