原文:Immediately-Invoked Function Expression (IIFE)
作者:”Cowboy” Ben Alman
或许你没有注意到,我是一个对术语比较在意的人。所以,在听到许多次 JavaScript「自执行匿名函数」(self-executing anonymous function)或者「自调用匿名函数」(self-invoked anonymous function)这两个流行但令人误解的术语,我觉得得将我的想法写下来。
本文实际上除了提供详细关于这个模式的原理之外,还推荐了我们之后如何称呼这个模式。当然,你可以跳过关于这个推荐的内容,直接查看「立即执行函数表达式」的例子,但是我建议你完整地阅读本文。
请注意,本文并非是那种「我就是正确的,而你是错误」的论调。我真诚的希望帮助人们了解潜在的复杂的概念,并且深感使用一致并且精确的术语是一种非常有效的促进人们相互理解的方法。
那么,到底本文旨在何处?
在 JavaScript 中,每一个函数被调用时都会创建一个执行上下文(execution context)。定义在函数内部的变量和函数都只能在这个执行上下文的内部访问到,所以函数提供了一种创建私有成员的便捷的方法。
|
|
[译注]:JSFiddle例子链接
在很多情况下,不论makeWhatever
函数返回的是什么样的值,你希望这个值在每次返回都是相同的,或者,你根本不需要函数返回一个值。
问题的本质
假若你定义了一个函数:function foo () {}
或者 var foo = function () {}
,你都获得了一个函数的引用变量。可以在在这个变量使用括号操作符(parenthese)来调用函数,即:foo()
。
|
|
上面代码执行后就会抛出一个错误(catch)。当脚本解释器在全局命名空间或者函数内部遇到关键字function
的时候,默认地,解释器认为这是一个函数声明语句(function declaraction),而非函数表达式(function expression)。如果没有明确地告知解释器function(){}()
是一个函数表达式,解释器将认为这是一个函数声明,并且声明没有指定函数的名字。由于函数声明时必需为指定函数的名称,所以解释器抛出语法错误(SyntaxError)。
函数,括号以及语法错误
有趣的是,既然是由于缺乏函数名称而导致语法错误,那么只要指定一个函数名字不就通过了?解释器依然会抛出一个语法错误提示,但却是由于不同的原因。当括号放置在函数表达式后,此括号即为括号运算符,表示调用函数;然而当括号放置在语句之后意味着分离括号前面与括号中的内容,此时括号仅仅做为分组表示(即用于改变运算的优先关系)。
|
|
你可以访问 Dmitry A. Soshnikove 对于此运算内容翔实的文章:ECMA-262-3 in detail. Chapter 5. Functions。
立即执行函数表达式(IIFE)
幸运的是,可以简单地修复上述的语法错误问题。被广为接受的修复方式是使用括号明确告诉解释器这个是一个函数表达式。因为在JavaScript中,括号无法包含语句。因此,当解释器遇到括号中的function
关键字时,将认为这是一个函数表达式,而非函数声明。
|
|
关于上述代码中括号的一个重要说明
虽然在上述那些括号可有可无(因为解释器可以正确地识别他们是函数表达式),但还是建议在赋值语句中带上括号,作为一种不成文的约定。
括号在这种情况下表示函数将会立即被执行,同时,执行结果返回的函数执行的结果而非函数本身。通过这个约定,碰到一个函数表达式时就没有必要滚动到函数表达式的末尾(函数可能非常长)去查看是否立即执行了这个函数。
根据以往的经验,编写清晰的代码在技术上可以避免JavaScript解释器抛语法错误异常,同时也可以避免遇到「屎一样的BUG」。
使用闭包(closures)保存状态
实参可以传递给函数名(通过函数声明),也可以传递给立即调用的函数表达式。同时,函数(outer)可以在内部定义另外一个函数(inner),此时内部函数(inner)可以访问外部函数(outer)的传递进来参数和变量(这种关系被称为「闭包」)。结合上述的两种特性,我们可以使用「立即执行函数表达式」来锁定变量值并保存状态。
如果想要了解更多关于「闭包」的内容,请访问:Closures explained with JavaScript。
|
|
注意上述代码中的最后两个例子,lockedInIndex
可以正确的使用i
的值。使用lockedInIndex
作为立即执行函数表达式的参数令代码看起来更易理解。
立即执行函数表达式的另外一个优点在于,由于立即执行函数表达式并未使用标识符来命名,即未命名或者匿名的,所以可以不污染全局命名空间的情况使用。
那么术语「自执行匿名函数」哪里不对?
在上文中已经多次提到我所建议的术语「立即执行函数表达式」,或者简写为「IIFE」,但是并未明确说明。我将他念成「iffi」(亦非?)。
什么是「立即执行函数表达式」?顾名思义,她是一个被立即执行的函数表达式。
我很希望看到JavaScript社区接受「立即执行函数表达式」(IIFE),在他们的文章中使用这个术语。因为我觉得IIFE让这个概念更加清晰,也因为术语「自执行匿名函数」并不准确:
|
|
希望上述的例子可以验证「自执行」这个词是令人误解的。虽然函数一样是执行了,但并非是函数执行她本身。同时,「匿名」也无须特别指出,因为一个立即执行函数表达式可以匿名也可以具有名字。最后,我比较喜欢「invoked」而非「executed」在于押韵,我想「IIFE」看起来念起来比「IEFE」顺。
这就是我的想法。
由于 arguments.callee
在 ECMAScript 5 strict mode 中已被弃用,所以从技术上说一个自执行匿名函数是不可能实现的。
模块模式(Module Pattern)
当我在论述函数表达式的时候,可以漏掉了模块模式。若你对JavaScript里面的模块模式不熟悉的话,那么查看文中第一个例子,那个例子就是接近于模块模式。之所以说接近,是由于模块模式返回的是一个对象,而第一个例子返回的是函数。若第一个例子返回值改成一个对象,也就通常实现单例模式(Singleton Pattern)的方法,如下代码所示:
|
|
[译注] 例子的JSFiddle链接
模块模式不仅仅强大并且简洁明了。使用很少的代码就可以有效地将方法和属性封装起来,与此同时不污染全局命名空间以及创建私有作用域。
扩展阅读
希望本文可以解答你的一些疑虑。当然,若阅读完本文之后你的疑问比之前更多了,你可以阅读以下列表的文章以探索更多关于函数以及模块模式:
- ECMA-262-3 in detail. Chapter 5. Functions - Dmitry A. Soshinikov
- Functions and function scope - Mozilla Developer Network
- Named function expressions - Named function expressions - Juriy “kangax” Zaytsev
- JavaScript Module Pattern: In-Depth - JavaScript Module Pattern: In-Depth - Ben Cherry
- Closures explained with JavaScript - Nick Morgan
最后感谢 Asen Bozhilov 和 John David Dalton 的技术建议以及Nick Morgan的深刻见解。如果你由任何想法,请在评论里发表,谢谢!