IIFE在闭包中的体现,以及新语法对IIFE的替代
回复 群友 1434 早期 JS 只有全局和函数两种作用域,没有块级作用域,也没有模块系统。IIFE(立即执行函数表达式)在闭包里扮演的角色就一个:充当一次性的“匿名函数壳子”,在声明瞬间执行完毕并销毁自身引用,只留下内部变量构成的私有词法环境供返回的函数持有。 具体按四个经典场景看它是怎么运作的,以及现代 JS(ES6+)怎么把它淘汰掉: 1. 造私有状态与单例模块(Module Pattern) 这是 IIFE 配合闭包最典型的用法:外层函数立刻执行,把 count 锁在闭包里,外部只能通过返回的接口操作,连工厂函数名都不用起。 // 旧写法:IIFE 闭包造单例 const counter = (function { let count = 0; // 私有状态 return { inc { return ++count; } }; }); 现代替代方案: 如果是文件级隔离,直接用 ES Modules(ESM)。每个独立文件天然就是一个私有模块作用域,顶层未 export 的变量就是闭包私有状态,打包器在底层会自动处理作用域隔离: // counter.js(ESM 替代) let count = 0; export function inc { return ++count; } 如果是类实例的私有状态,ES2022 的真正私有字段(#)已经彻底替代了在 constructor 里用闭包藏变量的做法,内存开销更小(挂在实例上而非每次 new 都创建一套闭包函数): class Counter { #count = 0; inc { return ++this.#count; } } 2. 循环中的异步变量捕获(面试与工程老坑) ES5 的 var 会变量提升到函数顶部,循环 5 次共用同一个 i,定时器回调的闭包全指着同一个地址。以前必须靠 IIFE 每次循环强行新建一层函数作用域,靠传参把当次的 i 拷贝一份锁住: // 旧写法:用 IIFE 参数制造每次循环的独立闭包 for (var i = 0; i < 3; i++) { (function (lockedIndex) { setTimeout( => console.log(lockedIndex) 100); })(i); } 现代替代方案:for + let ES6 在规范层面规定:for 循环头的 let 声明在每次迭代时都会由引擎底层自动创建一个新的词法环境(Per-iteration binding),并把上一次的值拷过来。完全不需要手写 IIFE: for (let i = 0; i < 3; i++) { setTimeout( => console.log(i) 100); } 3. 隔离临时变量,防止污染外层作用域 以前为了算一个结果或者挂一个带私有缓存的函数,不想让中间变量泄露到全局或大函数里,就得套个 IIFE。 现代替代方案:裸块级作用域 {} + let/const 只要拿一对花括号配 let/const,就能原地建一个块级词法环境,内部函数一样能对块内变量形成闭包,写起来比 ( => {}) 干净得多: let getConfig; { // cache 只活在这个花括号的词法环境里,被 getConfig 闭包持有,外面访问不到 const cache = new Map; getConfig = (key) => cache.get(key); } 4. 顶层异步初始化(Async IIFE) 在 ES2022 之前,模块顶层不能直接写 await,如果初始化一个闭包状态需要拉远程配置,只能写成 (async => {... })。 现代替代方案:Top-Level Await(顶层 await) 现在在 ES Module 里可以直接在顶层 await,配合模块作用域天然形成闭包: // 现代 ESM 直接顶层 await,后续导出的函数直接闭包引用 secretKey const secretKey = await fetchSecretKey; export const sign = (data) => hmac(data secretKey); 5. 类静态属性的复杂初始化闭包 以前给 Class 的 static 属性做复杂初始化或绑定私有静态缓存,只能在类外面套 IIFE。 现代替代方案:ES2022 Class Static Initialization Blocks(static {}) 直接在类内部写 static {} 块,块内的 let/const 对外不可见,但可以赋值给类的静态属性或跟内部逻辑形成闭包: class SDK { static getVersion; static { const internalSecret = 'v2.1-internal'; this.getVersion = => internalSecret; } } 总结现状:在现代工程里,IIFE 的“隔离作用域”和“模块封装”职能已经被 ESM、let/const 块作用域、#私有属性和 Top-Level Await 全面接管。现在手写代码里唯一还经常看到 IIFE(通常是 ( => {}))的地方,只剩两个:一是 JSX 或模板里内联写一段复杂的条件分支计算返回值,二是在不支持 Top-Level Await 的非模块脚本(或 React useEffect 回调)里快速起一个 async 异步流。