ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IIFE不用括号也能写?五种写法与底层原理一次讲透

IIFE不用括号也能写?五种写法与底层原理一次讲透 不做铺垫了直接说结论(function(){})()这种写法确实不是 IIFE 的唯一形态前面那个括号不是语法规定非写不可的“仪式”它只是最常见、最稳妥的“变形手段”。IIFE 难就难在“立即调用”和“函数表达式”这两个条件同时满足而 JavaScript 的解析规则决定了function关键字出现在语句开头时会被当成函数声明函数声明又不能直接跟调用括号。所以绕开这个限制的方法不止一种括号只是最直观的那一种。这篇文章会把“不用括号也能跑”的几种方式全部拆开揉碎讲明白顺便说清楚什么时候该用、什么时候别用、面试被问到该怎么答。1. IIFE的本质不是括号是“表达式”1.1 先从一次翻车现场说起先讲个我早几年面试别人时遇到的场景。候选人写在白板上的代码是这样的function() { console.log(hello); }()我一看就乐了这明显是没搞清楚 IIFE 的底层原理。这行代码一旦放进浏览器直接给你甩一个SyntaxError: Function statements require a function name。原因很简单function当语句的开头时JS 引擎会强制把它解析成“函数声明”函数声明必须有名字所以没名字直接报语法错误。就算你给它补个名字function foo() { console.log(hello); }()这也跑不起来会报SyntaxError: Unexpected token )。为什么因为函数声明不会返回函数本身你写了一个声明语句然后试图直接在声明后面接一对调用括号引擎根本不知道你要调用谁。函数声明只负责“定义一个名叫 foo 的函数”它不是一个值所以后面跟()没有意义。这两次翻车其实已经暴露了 IIFE 的全部秘密想要“定义完立刻调用”必须让函数以“值”的形式存在。只有函数表达式才是一个值才能被()调用。括号的真正作用就是把function从“语句的开头”这个位置挪走让引擎把这个function解析成表达式。1.2 表达式和语句的区别是决定性的JavaScript 里有两类代码“表达式”和“语句”。表达式的特点是会产出一个值比如1 2、abc.length、() {}都是表达式。语句的特点是执行一个动作比如if (x) {}、for (;;) {}、var a 1;它们本身不产出值。function关键字恰好有两副面孔放在语句开头的时候是“函数声明”是语句放在表达式位置的时候是“函数表达式”是值。JS 引擎怎么判断你是哪一副面孔主要看语法上下文最关键的一点就是function是不是出现在一个“表达式允许出现”的位置。(function(){})之所以能成立就是因为(先出现引擎已经确定这里需要一个表达式所以后面的function(){}自然被当成函数表达式。这也是括号的本质作用——把函数声明硬掰成函数表达式。既然核心是“让 function 出现在表达式位置”那能做到这件事的就绝不止括号一种方式。任何能改变“function作为语句开头”这个先天条件的手段理论上都能实现 IIFE。2. 不用括号的N种姿势一元运算符大法2.1 用!、、-、~强行转表达式这是老鸟最常用的“不用括号版 IIFE”核心就是利用一元运算符。一元运算符的特点是只需要一个操作数而且它跟操作数之间没有语法上的歧义。比如!function() { console.log(bang); }()这行代码是能正常跑的。你可能会问为什么这里不带括号就可以因为!后面优先需要一个表达式引擎在这里看到function(){}会直接按函数表达式解析不会按函数声明处理。然后()跟在函数表达式后面立即调用最后!把调用结果取反。在控制台会输出bang然后显示true因为函数没有返回值undefined取反是true。同理、-、~也可以function() { console.log(plus); }() -function() { console.log(minus); }() ~function() { console.log(tilde); }()这三个运算符的思路完全一样用运算符逼迫解析器把后面的function当作表达式。区别只在返回值会怎么被处理。会把undefined转成NaN-也是NaN~按位取反~undefined结果是-1。对 IIFE 本身来说返回值往往无关紧要但如果你在一个表达式上下文里用这些写法返回值会影响整个表达式的值这点后面会讲。另外还有一个很冷门的voidvoid function() { console.log(void); }()void运算符的作用是计算后面的表达式然后永远返回undefined。用它来包 IIFE 其实语义上最“干净”因为它明确表示“我不需要这个表达式的值”。在一些老代码里能看到void function(){}()的写法就是图一个语义清晰。2.2 运算符版本和括号版本的细微差异你可能关心一个问题这些写法和经典括号写法完全等价吗严格说不完全等价差异出现在“调用之后”。括号版(function(){})()整个表达式的结果就是函数调用后的返回值外面不会再被套一层运算符。一元运算符版则不同函数调用完它的返回值还会经过!//~/void的加工。假如函数返回了一个有意义的值const a (function() { return 42; })(); // a 42 const b !function() { return 42; }(); // b false42 被取反 const c function() { return 42; }(); // c 42 const d ~function() { return 42; }(); // d -43所以不是简单的“换个写法结果一样”而是“IIFE 照样执行但结果经过了额外运算”。这在大多数实际场景下不重要因为 IIFE 本来就经常只用来制造一个作用域并不关心值。但如果后续代码要用到 IIFE 的返回值就得留个心眼。我自己的习惯是随手演示或者写片段逻辑时喜欢用!function(){}()或void function(){}()尤其是写“立即执行 不需要返回值”的场合。不是因为它比括号版更高级而是写起来有一种“一行代码搞定”的爽感。但工程代码里我基本不会这么写原因后面第 4 节会详细说。2.3 还有一个容易被忽略的async function直接调用严格说async function() {}()不能直接跑但你确实可以用更短的写法实现“定义即执行”(async function() { console.log(async iife); })()这里还是用了括号。真正的骚操作是你在模块代码里可以直接写顶层 await但这不属于 IIFE 范畴了。更贴近标题的做法是如果你使用 ES6 的块级作用域理论上也可以用let或const加调用语句做成“伪 IIFE”{ const name inside; console.log(name); }这段代码不需要任何括号也能做到“局部作用域 立即执行”。但严格意义上它不叫 IIFE因为它没有一个“函数表达式”被调用。可它解决的问题跟 IIFE 几乎一样——隔离变量避免污染全局。这算是对“IIFE 精神”的一种现代替代。如果非要用“真正的函数立即执行”且不带括号还可以用newnew function() { console.log(new iife); }new后面跟函数表达式是完全合法的引擎同样会把function(){}解析成表达式。构造函数没有参数时可以省略调用括号于是你就得到了一行极其冷门的“new 匿名函数立即执行”。这个写法每次会创建一个新对象返回值是这个对象所以如果拿它当 IIFE 用内存上会有一个多余的对象分配。我见过有面试官出这道题的但现实中基本没人这么写。3. 底层原理解析器的“陷阱”与“机会”3.1 JS 引擎为什么对语句开头的 function 这么敏感学过编译原理的都知道解析器判定一段代码是声明还是表达式靠的是文法规则。ECMAScript 规范里FunctionDeclaration和FunctionExpression的语法产生式不同但起始符号都是function这就产生了歧义。规范通过“上下文”消解歧义当解析器处在一个“需要语句”的位置看到function就优先按函数声明解析当处在“需要表达式”的位置就按函数表达式解析。听起来有点绕打比方说同样是一句“苹果”在水果店老板耳朵里是“我要买苹果”在园艺师耳朵里是“我要种苹果树”。一个字词在不同上下文里被赋予了不同的角色。JS 里的function就是这样语法位置决定了它的身份。这也解释了为什么(function(){})能行而function(){}不能行外层括号让解析器先进入“处理括号内表达式”的状态它需要一个表达式于是函数表达式就诞生了。这也是为什么所有经典 JavaScript 书籍都告诉你 IIFE 要加括号——不是括号本身有什么魔力而是它改变了语法上下文。3.2 一元运算符为什么也能改变上下文一元运算符同样能改变上下文。当解析器看到!这个 token 时它明确知道后面必须跟一个表达式一元表达式的操作数。于是读到一个function关键字时它不再犹豫直接按函数表达式解析。一次性解决了“匿名”和“立即调用”两个问题。类似的手段还有等号赋值var fn function() { console.log(assign); }()这也是合法的 IIFE因为右边要求一个表达式。只是这种写法罕见因为你还额外声明了一个变量fn而这个变量接住的是函数的返回值不是函数本身。如果你写var fn function() { console.log(assign); }这就不算 IIFE 了只是赋值了一个函数。要“立即调用”必须让函数表达式后面跟上调用括号。所以这个变体本质上和!function(){}()是同一类靠一个“要求表达式”的上下文来触发函数表达式解析。再看一个更隐蔽的return function() {}()。这在函数内部合法因为return后面必须是一个表达式function自动按表达式解析然后直接被调用。同样的还有typeof function() {}()、x ? function(){}() : 0等都属于“利用上下文”的变体。3.3 关于“自动分号插入”的一个冷知识很多人忽略 ASI自动分号插入对 IIFE 的影响。经典括号版 IIFE 有一个著名的坑上一行代码没有分号时可能会被解析成函数调用。比如const a 1 (function(){ console.log(x) })()这段代码看起来是两行第一行const a 1第二行一个 IIFE。但因为没有分号JS 引擎会把第二行开头的(当作去调用a这个值——试图执行1(...)直接报TypeError: a is not a function。这种坑在“不用括号版”里会变成另一种形态。如果你用!function(){}()这种写法就不会和上一行粘连因为上一行结束时如果不写分号!这个 token 和上一行的表达式之间没有合法的延续关系引擎只能插入分号。某种程度上“一元运算符开头”天然规避了 IIFE 和上一行粘连的问题。这也是我写一些零散脚本时偏爱!的原因之一省心。但反过来说如果上一行以)结尾后面接!function(){}()也不会粘连因为)!之间无法成为某个表达式的延续。这就是 ASI 世界里的一点小确定性。4. 工程实践这些骚操作到底能不能用4.1 可读性才是最大的坑先把话说清楚所有“不用括号的 IIFE”都只是在语法层面可行在工程层面它们大多属于“写给面试官看”的炫技。带团队的时候如果有人在业务代码里给我写一个!function(){}()我不会说他错但我会要求他改成括号版或者干脆换成 ES 模块。原因很简单代码是写给人看的。(function(){})()这个形态全世界的前端都认识一眼就知道“这是一个立即执行的函数”。!function(){}()虽然也短但如果混在一堆逻辑里阅读者要多想一步“这里为什么有个感叹号”甚至可能误以为你在做取反运算。代码的沟通成本上升维护性下降。工程上还有一个 lint 层面的问题。ESLint 的no-extra-parens规则有时会跟 IIFE 的括号冲突一些老的配置会报错导致开发者被迫改成void function(){}()。这个场景在 2015 年前后比较常见现在规则已经默认对 IIFE 放行了。但如果你还在维护老项目遇到 lint 报错时可以用一元运算符版本绕过去。4.2 现代替代品块级作用域和 ES Module其实今天你在现代前端项目里已经很少需要手写 IIFE 了。ES6 的let/const本身就带块级作用域一个{}就能隔离变量{ let count 0; count; console.log(count); }这个写法虽然没有“函数”但达成了 IIFE 的核心诉求——制造一个封闭作用域。因为你不需要函数内的this绑定也不需要return值块级作用域完全够用。这和 IIFE 相比还更轻量没有函数调用开销。再往上走ES Module 天生就是模块作用域。每个.js文件里的顶层变量不会自动挂到window上文件本身就是隔离的。这也是为什么现代项目中 IIFE 的使用频率断崖式下降——它要解决的问题语言特性已经原生解决了。但 IIFE 并没有死。打包工具的产物比如 webpack、Rollup 的 runtime里仍然大量使用 IIFE 来包裹模块避免变量泄漏到全局qiankun这类微前端框架在加载子应用时也会用到函数作用域来做沙箱隔离。所以理解 IIFE 的原理不是考古而是基本功。4.3 面试被问到这道题怎么答才算过关“IIFE 不用括号也能跑吗”这道题如果是面试官抛出来的他想考察的绝不只是“你知道几个运算符”而是你有没有真正理解函数表达式和函数声明的区别。按这个顺序回答基本能拿分第一层指出function关键字在语句开头会被解析为函数声明声明不能匿名也不能直接调用。这是根因。 第二层说明 IIFE 成立的前提是函数先变成表达式括号、一元运算符、赋值等都可以做到。 第三层现场写一个!function(){}()并解释为什么这样能跑。 第四层补充工程层面的观点——括号版可读性最好面试或写库时可以聊版本差异但业务代码优先标准写法。能讲到第四层的人说明不是背题是真懂。面试官如果追问“void function(){}()和!function(){}()有什么区别”你再说出“一个返回 undefined一个返回布尔取反后的值只看 IIFE 内部执行则无差异”就够了。4.4 一行代码的真正价值工具函数与脚本场景虽然业务代码不推荐但“一行代码搞定”这类技能在特定场景下是真香的。比如你在控制台调试想快速注入一段逻辑或者写一个 npm 脚本不想引入额外文件或者在做一些自动化小工具时用!function(){}()能少敲两个字符还能避免 ASI 粘连问题。再比如你要在页面上临时验证某个 polyfill 是否生效!function() { if (!window.Promise) console.warn(no promise); }()这在控制台里敲起来非常顺。配合void写法还能表达“我知道这里不需要返回值”的语义代码像注释一样清楚。还有一个常见场景是给外部脚本做安全包裹。如果你写一个第三方 SDK需要把内部变量全部藏起来IIFE 仍然是最经典的手段。这时候用括号版还是void版取决于团队的代码规范。我个人见过不少 SDK 源码写的是!function(global){ ... }(this)就是看中!开头能规避 ASI 粘连同时比(function(){})()少一个字符。反正这个函数本来也不需要返回值多一个!无伤大雅。5. 常见问题与排查技巧实录5.1 为什么我的 IIFE 报错Unexpected token报Unexpected token的一般是这两种情况一种是函数声明没名字还说“我明明写了函数”。检查一下function是不是在语句开头若是请加括号或者一元运算符。另一种是function(){}后面直接跟()但整体不在表达式上下文里比如if (true) function(){}()if后面跟的是语句function直接被当函数声明于是匿名报错。正确做法是先包成表达式再调用。还有新手容易犯的错IIFE 内部忘记加分号。虽然 ASI 会帮你擦屁股但在 IIFE 结尾后如果紧接着下一行开头的字符是(、[、、、/、-就可能引发粘连问题。这也是为什么很多老项目规范要求“IIFE 前面必须加分号”的原因。5.2 为什么我加了括号还是报错加了(function(){})还报错大概率是括号放错了位置。比如把调用括号放到了外层括号外面(function() { console.log(x); }())这种叫“先执行再收尾”也是合法的规范里管这个叫“包围调用整体”。它和(function() { console.log(x); })()在语义上完全等价。如果报错大概率是函数体内部语法有问题。另一个容易错的点是参数(function(a) { console.log(a); })(1)这没问题。但写成(function(a)(1))就废了语法上完全不成立。参数一定要放在函数定义那一对括号里调用参数放在第二个括号里。还有一种隐蔽错误箭头函数写法。() {}作为 IIFE 时箭头函数表达式本身没有自己的arguments如果你在内部访问arguments会报错。另外箭头函数在语法上不需要function关键字所以“不用括号”的玩法在箭头函数上完全不适用也没有必要。5.3 关于this的经典坑IIFE 里的this指向哪里非严格模式下普通函数里的this指向全局对象window。严格模式下会指向undefined。这跟是不是 IIFE 没关系只跟“你怎么调用它”有关。但因为 IIFE 看起来像一个孤立的函数很多人会误以为它里面的this指向什么特殊的东西这是个高频误解。同时用一元运算符改写的 IIFE 不改变this规则。!function(){}()里函数的this仍然是普通函数调用规则下的值不会因为前面多了一个!就变成undefined或其他。真正改变this的是箭头函数和call/apply/bind。如果你在 IIFE 里想要稳定的this一句(function(){ /* ... */ }).call(someObj)才是正解。5.4 性能有差异吗从引擎角度看括号版和一元运算符版在解析阶段的 token 流略有不同但执行阶段没有任何本质差异。函数照样创建照样调用。所谓性能差异在真实业务里测不出来完全没有优化价值。唯一要留意的是new function(){}这种写法会创建额外对象如果你在热路径里写这种代码那确实是自己给自己挖坑。5.5 一份速查表写法是否合法执行结果语义说明(function(){})()合法函数返回值经典写法推荐(function(){}())合法函数返回值等价经典写法!function(){}()合法返回值取反一元运算符版function(){}()合法数值转换一元运算符版void function(){}()合法始终 undefined语义最清晰的不带括号版function(){}()语法错误无匿名函数声明new function(){}合法新对象冷门写法不推荐{ let x 1; }合法undefined块级作用域替代方案现代推荐5.6 再分享一个配合版本号强制刷新的小技巧这个和 IIFE 没有直接关系但既然说到“一行代码”和前端工程顺带分享一个我常用的强制刷新思路发布时给 JS 资源加上版本号查询参数比如app.js?v20241010。结合 IIFE 包裹的入口文件你可以在文件开头写一段自执行逻辑读取当前版本号若与本地缓存版本不一致就清掉缓存并location.reload()。这个玩法在纯前端项目里很实用本质上是用“立即执行的函数”干一件初始化的事不污染全局。代码大概长这样!function(version) { const cached localStorage.getItem(app_version); if (cached cached ! version) { localStorage.clear(); location.reload(); } localStorage.setItem(app_version, version); }(20241010);这种写法如果不用 IIFE你就得多声明一个全局变量然后手动调用代码会变啰嗦。用 IIFE 一包内部逻辑完全隔离还顺手演示了“带参数的 IIFE”和“运行时传值”的使用场景。遇到“前端强制刷新页面”之类的需求可以参考这个思路。6. 老鸟的几句体己话写了这么多年前端我最大的感受是这类“骚操作”背后藏的语法原理比操作本身值钱得多。你学会了!function(){}()可能一辈子也用不上几次但你理解了“JS 引擎通过上下文区分函数声明和函数表达式”你就能看懂无数源码里的稀奇写法也能在面试时把一个问题讲出层次。我个人在实际操作中的体会是写库和写工具时我会偶尔用void function(){}()来明确表达“不需要返回值”顺便避免 ASI 粘连但业务代码里我一定是老老实实写(function(){})()因为团队协作时“一眼看懂”永远比“少打两个字符”重要。另外如果你在维护老项目看到同事写的!function(){}()不要急着说他错——它没错只是风格不同。最后再分享一个小技巧当你下次在控制台里懒得敲括号时直接在function(){}前面加一个!随手调跑完就走。这种一行代码的真实爽感用过的都懂。
返回列表