☰
jstips 第 25 期解读:深入理解 JavaScript 立即执行函数表达式(IIFE)
2026/10/8 6:56:23 网站建设 项目流程
  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

立即执行函数表达式(Immediately Invoked Function Expression,简称 IIFE,常被戏称为 "Iffy")是 JavaScript 中一个看似简单却用途深远的语法结构。本篇基于 jstips 仓库中的英文原帖 _posts/en/javascript/2016-01-25-Using-immediately-invoked-function-expression.md(中文版见 _posts/zh_CN/javascript/2016-01-25-Using-immediately-invoked-function-expression.md,繁体中文版见 _posts/zh_TW/javascript/2016-01-25-Using-immediately-invoked-function-expression.md),系统讲解 IIFE 的语法原理、命名变体、典型应用场景与性能考量。读完本文,你将掌握 IIFE 的完整写法,理解它如何借助函数表达式与作用域机制实现变量隔离,并能在模块封装、初始化逻辑、循环闭包等真实场景中熟练运用。

什么是 IIFE

IIFE 全称 Immediately Invoked Function Expression,即"立即执行函数表达式"。从字面拆解:

  • Immediately Invoked:定义之后立即被调用;
  • Function Expression:它首先是一个函数表达式,而非函数声明;
  • 它通常是一个匿名函数,因此经常被开发者昵称为 "Iffy"。

原帖给出的最小示例是这样的:

(function() { // Do something } )()

这段代码定义了一个匿名函数,将其包裹在括号中,并在定义的同时通过末尾的()立即调用执行。运行到这一行时,函数体内的代码会马上执行一次,且不会留下任何可被外部引用的函数名。

括号的关键作用:把函数声明变成函数表达式

IIFE 中那对包裹函数的圆括号是整个语法的核心。原帖明确指出:包裹匿名函数的这对括号,将匿名函数转换成了一个函数表达式(function expression)或变量表达式(variable expression)。

为什么必须要这对括号?这需要回顾 JavaScript 的解析规则。当解释器看到以function关键字开头的语句时,会将其视为函数声明——声明必须有一个名字,而且不能直接跟()调用(会产生语法错误)。而一旦把函数体放进括号里((function(){ ... })),function就不再位于语句开头,解析器便会将整个括号内容当作表达式来求值,此时它返回一个函数对象,后面紧跟的()才能对其执行调用。

这与仓库中另一篇关于变量提升(Hoisting)的 tip 密切相关。该 tip 指出,函数声明会被整体提升(hoisted)到作用域顶部,而函数表达式(如var functionVar = function(){ ... })只提升声明、不提升赋值,因此在赋值之前调用它会得到TypeError: undefined is not a function。IIFE 利用的正是"表达式先求值、后调用"的机制:括号内的函数作为一个整体先被求值成函数对象,紧接着被调用,既不需要名字,也不会被提升逻辑干扰。

所以,一个 IIFE 在宏观上等价于"创建了一个临时函数对象并立即执行",它的完整形式可以写作:

(function() { // 函数体 })();

末尾的分号不是必须的(取决于上下文),但建议保留,以避免与前面语句产生解析歧义。

其他合法的 IIFE 写法

除了用圆括号包裹,JavaScript 还允许用任何能把function从"语句起始位置"变成"表达式的一部分"的操作符来构造 IIFE。常见的替代写法包括:

// 一元操作符形式 !function() { console.log("立即执行"); }(); ~function() { console.log("立即执行"); }(); +function() { console.log("立即执行"); }(); -function() { console.log("立即执行"); }(); // void 操作符形式 void function() { console.log("立即执行"); }();

这些写法效果相同,但会改变函数的返回值(例如!会得到布尔值取反、void恒得undefined),实际项目中用得最多的仍是圆括号形式,因为它语义最清晰、不改变返回值。仓库中同样涉及一元操作符与返回值语义的讨论可参考 2020-11-04-what-is-a-void-operator.md 和 2016-04-05-return-values-with-the-new-operator.md。

命名 IIFE:IIFE 也可以有名字

虽然 IIFE 最常见的形式是匿名的,但原帖特别强调:我们同样可以创建"命名"的立即执行函数表达式。请看原帖的完整示例:

(someNamedFunction = function(msg) { console.log(msg || "Nothing for today !!") }) (); // Output --> Nothing for today !! someNamedFunction("Javascript rocks !!"); // Output --> Javascript rocks !! someNamedFunction(); // Output --> Nothing for today !!

这段代码有两点值得细说:

  1. 左侧赋值:someNamedFunction = function(msg){ ... }把函数表达式赋值给了全局变量someNamedFunction。因此虽然这个函数是"立即执行"的,但它的引用被保留了下来,之后可以像普通函数一样多次调用(someNamedFunction("Javascript rocks !!")、someNamedFunction())。
  2. 默认参数技巧:函数体中的msg || "Nothing for today !!"是一个短路求值(short-circuit evaluation)的经典用法——当msg为空值时返回默认文案。关于短路求值的详细讨论可参考仓库中的 _posts/en/javascript/2016-01-27-short-circuit-evaluation-in-js.md。运行结果:
    • 立即执行时msg为undefined,输出Nothing for today !!;
    • 传入"Javascript rocks !!"时原样输出;
    • 再次无参调用时又输出默认文案。

这种"命名 IIFE"模式常被用于模块初始化:既要在加载时立刻执行初始化逻辑,又要暴露若干可供外部调用的函数接口,相当于手工实现一个极简模块系统。

IIFE 的核心价值:隔离作用域,避免全局污染

理解 IIFE 的价值,需要先理解 JavaScript 的作用域规则。在 ES6 之前,JavaScript 只有全局作用域和函数作用域,var声明的变量没有块级作用域。仓库中的 Basics declarations 一篇给出了非常直观的对比实验:

var y, x = y = 1 //== var x; var y; x = y = 1 console.log('--> 1:', `x = ${x}, y = ${y}`) //--> 1: x = 1, y = 1 ;(() => { var x = y = 2 // == var x; x = y = 2; console.log('2.0:', `x = ${x}, y = ${y}`) //2.0: x = 2, y = 2 })() console.log('--> 2.1:', `x = ${x}, y = ${y}`) //--> 2.1: x = 1, y = 2

这个例子中的;(() => { ... })()正是一个 IIFE(使用了箭头函数形式)。它揭示了一个极易踩坑的事实:在 IIFE 内部写var x = y = 2时,x因为带var而被限制在函数作用域内,而y因为没有var前缀,被隐式声明到了全局作用域。于是 IIFE 执行完毕后,外部y的值从 1 被改成了 2,而x保持 1 不变。

这说明两点:

  • IIFE 内部默认拥有独立作用域:在函数体内用var声明的变量不会泄漏到外部;
  • 必须显式使用var(或let/const):否则会通过隐式全局变量污染全局命名空间。

而关于隐式全局变量,仓库中的 use strict and get lazy 一篇给出了更严厉的约束:在严格模式下,"变量只能通过var(以及let/const)声明引入",未声明的赋值会直接抛出错误,而不是悄悄创建全局变量。将二者结合,最稳健的模块封装写法是:

(function() { "use strict"; var privateCounter = 0; // 外部无法访问 window.increment = function() { // 只暴露需要公开的部分 return ++privateCounter; }; })();

这样,IIFE 负责创建私有作用域,严格模式负责兜底阻止隐式全局变量,二者配合实现了真正可靠的封装。

IIFE 的典型应用场景

结合原帖"有一些特别重要的用途"的表述,IIFE 在实战中主要有以下几类用途:

1. 模块化封装与私有状态

在 ES Modules 出现之前,IIFE 是前端构建"模块"的事实标准(如 jQuery 插件、UMD 包装)。通过闭包,IIFE 内部变量对外部完全隐藏,只有显式返回或挂载的对象可见,从而避免与全局环境相互干扰。关于闭包的更多细节可参考仓库中的 _posts/en/javascript/2017-06-14-closures-inside-loops.md。

2. 循环中的闭包陷阱

经典的面试题"for 循环 + setTimeout 输出 i"的解法之一就是 IIFE:

for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }

每次迭代通过 IIFE 创建一个新的函数作用域,并把当前的i作为参数j传入,从而为每个回调捕获独立的j值。这正是"立即执行 + 参数传递"的典型组合。

3. 初始化逻辑的一次性执行

只需运行一次、且不希望留下任何全局足迹的代码(事件绑定、配置读取、polyfill 注入等),非常适合放进 IIFE。原帖中的"命名 IIFE"示例本质上也是初始化场景——执行一次并保留句柄。

4. 作为现代工具链的编译产物

使用 Babel 等工具转译 ES6 模块代码时,生成的 CommonJS/UMD 包装通常就是 IIFE 结构;构建工具(如 Webpack 的 runtime)也会大量使用 IIFE 来隔离每个模块的作用域,避免模块间的变量互相泄漏。这一点从 Basics declarations 中的;(() => { ... })()写法可以直观感受到——现代代码中箭头函数形式的 IIFE 已非常普遍。

IIFE 与现代语法的关系:箭头函数与块级作用域

原帖发布于 2016 年初,彼时 ES6 刚刚普及。今天再看,IIFE 的许多职责正在被新语法"接管":

  • 箭头函数:(() => { ... })()与(function() { ... })()语法等价,只是箭头函数不绑定自己的this,会从外层词法环境继承this。仓库中的 Fat Arrow Functions 一篇详细讲解了这一特性,指出箭头函数"从周围词法上下文捕获this",因此在需要保留外部this的场景下,箭头函数形式的 IIFE 更加顺手;而需要独立this(如构造逻辑)时则应继续使用传统function形式。
  • let/const块级作用域:在for循环中使用let i声明,每次迭代天然生成独立绑定,已不再需要 IIFE 来"救场";模块系统(ES Modules)也让"用 IIFE 造模块"不再是刚需。但 IIFE 作为"立即执行一段隔离代码"的通用工具,在各类初始化、工具函数、防御性编程中依然大量存在,值得完全掌握。

参数、返回值与性能考量

给 IIFE 传参

IIFE 末尾的()就是一次普通的函数调用,可以传入任意参数:

(function(window, document, undefined) { // 在函数内部使用局部别名,既缩短书写又提升一点访问速度 })(window, document);
  • 传入的实参在 IIFE 内部成为局部变量,外部修改不会影响内部;
  • 关于"引用如何传递",仓库中的 Know the passing mechanism 一篇有详细说明:JavaScript 对对象传递的是"引用值"(call-by-sharing),IIFE 的参数传递同样遵循这一规则。

接收 IIFE 的返回值

IIFE 是一个表达式,它的调用结果可以被赋值、参与运算:

var counterModule = (function() { var count = 0; return { inc: function() { return ++count; }, dec: function() { return --count; } }; })();

这里 IIFE 返回一个包含两个方法的对象,赋值给counterModule,外部只能通过这两个方法读写count——私有变量 + 公开接口的模块模式就此完成。这种"返回对象以支持链式调用"的思想在仓库的 _posts/en/javascript/2016-01-20-return-objects-to-enable-chaining-of-functions.md 中也有呼应。

性能与注意事项

原帖在结尾提到了性能话题,并附有 jsPerf 基准链接(该外部页面现已不可访问,此处仅作历史参考)。结合实践,关于 IIFE 的性能与工程注意事项可以总结为:

  • 创建与调用的开销:IIFE 每次执行都要创建新的函数对象和作用域,因此在超高频循环体内滥用 IIFE 会产生不必要的开销。现代 V8 等引擎对此有大量优化,实际影响通常很小,但写法上仍建议"循环体内部尽量复用已定义的函数,而不是反复创建 IIFE"。
  • 代码可读性:IIFE 的首要价值是作用域隔离而非性能优化;为了可读性,建议始终写清括号与分号,必要时给出注释说明这段立即执行的意图。
  • 与严格模式的位置关系:"use strict"若放在 IIFE 内部,则只对该 IIFE 生效,不会影响外部代码——这是把严格模式"局部化"的常用手法,可避免 use strict and get lazy 中提到的"构建链拼接导致全文件意外进入严格模式"的副作用。

小结

IIFE 是 JavaScript 中"小而重要"的语法设施:一对括号把函数声明变为函数表达式,末尾的()让它立即执行,二者合在一起创造了独立的函数作用域。原帖的核心要点可浓缩为:

  • 基本形式(function() { ... })(),匿名且立即执行;
  • 括号的作用是制造函数表达式,从而绕开"语句开头function视为声明"的解析规则;
  • 也支持命名形式(someNamedFunction = function(msg){ ... })(),执行后仍可通过变量名再次调用;
  • 主要用途是作用域隔离、模块封装、初始化执行,并常与严格模式、闭包、短路求值等特性协同工作。

在现代 JavaScript 中,IIFE 的部分场景被块级作用域、箭头函数和模块系统取代,但理解其"表达式 + 立即调用"的本质,依然是理解闭包、模块化和各类工具链编译产物的基础。仓库内还有多篇与之互补的 tip 值得继续阅读:作用域相关的 Basics declarations、闭包相关的 closures-inside-loops、箭头函数相关的 Fat Arrow Functions,以及严格模式相关的 use strict and get lazy。

  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询