先记住这个答案
IIFE(Immediately Invoked Function Expression)是定义之后立刻执行的函数表达式,比如 (function () { var a = 1 })()。外层那对括号的作用是让解析器把 function 当成表达式而不是声明,这样后面才能接 () 直接调用。它的用途主要有三个:隔离变量不污染全局、用闭包保存私有状态、把一段只跑一次的初始化逻辑包起来。在 let、const 和 ES 模块普及之前,它是 JavaScript 里模块化的主要手段。
- 外层括号让
function被当成表达式,才能直接调用 - 里面的
var和函数不会漏到外面 - 返回函数时,闭包能留住私有状态
- 现代用法:async IIFE、一次性计算初始值
用 IIFE 做一个带私有状态的模块
jQuery 时代很多库都是这么写的:一个 IIFE 包住全部代码,只把公开的方法返回出去。
const counter = (function (initial) {
let count = initial // 外面访问不到
return {
increment() {
count += 1
},
value() {
return count
}
}
})(10) // 立即调用,传入初始值
counter.increment()
console.log(counter.value())
console.log(typeof count) // 外层根本没有 count查看输出与解释
11
undefined函数只执行了一次,执行完 count 本应随之销毁,但返回的 increment 和 value 还引用着它,所以它一直保留在闭包里。外面拿到的 counter 只有两个方法,想改 count 只能通过 increment,这就是所谓的模块模式。
参数 initial 也是常见用法。以前的库经常写成 (function ($, window) { ... })(jQuery, window),把全局对象当参数传进去,函数内部用短名字访问,还能防止外面改了 $ 影响内部。
为什么必须加括号
一行以 function 开头时,解析器会把它当成函数声明。函数声明必须有名字,而且声明后面不能直接跟 () 调用,所以 function () {}() 会直接报语法错误。
- 其它让它变成表达式的写法
- 只要让
function出现在「期望一个值」的位置,它就是表达式。括号包住整个函数(function () {})()和括号包到调用后面(function () {}())效果一样。前面加一元运算符也行,比如!function () {}()、void function () {}(),压缩后的库代码里经常见到这种写法,因为能省一个字符。 - 开头的分号是做什么的
- 老项目里常见
;(function () {})()。如果上一个文件末尾没写分号,拼接之后会变成a = b(function () {})(),上一行的值被当成函数来调用,直接报错。前面补一个分号就能把两段代码隔开。现在用打包工具和格式化工具,这个问题已经很少遇到。
现在还在用的场景
变量隔离这件事,现在用一对花括号加 let 就能做到,文件级别的隔离有 ES 模块。所以 IIFE 不再是必需品,但下面几种场景仍然顺手。
第一种是 async IIFE。在不支持顶层 await 的环境里,比如 CommonJS 的 Node 脚本,常写 (async () => { await main() })() 来获得一个能用 await 的入口。注意它返回的是 Promise,错误要用 .catch() 接住,否则会变成未处理的拒绝。
第二种是一次性计算一个常量。比如 const config = (() => { ... return result })(),把中间用到的临时变量关在函数里,外面只留一个 const,比先声明 let 再在后面赋值更清楚。
容易答错的地方
- 以为 IIFE 执行完所有变量都释放了
- 没被引用的局部变量会释放,被返回的函数引用着的变量会一直存活。模块模式能工作,靠的就是这一点。
- 把 IIFE 当成安全沙箱
- IIFE 只隔离变量名,里面的代码照样能访问
window、修改全局对象、发请求。它解决的是命名冲突,不是安全隔离。
面试官还会怎么问?
块级作用域能完全替代 IIFE 吗?
单纯隔离变量可以,{ let a = 1 } 就够了。但块不能接收参数,也不能返回值。需要把计算结果交给外层变量、或者要用 await 的时候,IIFE 还是更方便。
IIFE 和 ES 模块有什么区别?
IIFE 靠函数作用域隔离,对外暴露内容要么返回,要么挂到全局变量上,依赖关系全靠加载顺序。ES 模块本身就是独立作用域,用 import 和 export 明确声明依赖,打包工具可以据此做静态分析和 tree shaking。
箭头函数能写 IIFE 吗?
能,写成 (() => { ... })(),括号必须把箭头函数整个包住。它和普通函数 IIFE 的区别在于没有自己的 this 和 arguments,会用外层的。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。