译者注:立即执行函数表达式(mmediately-Invoked Function Expression,a.k.a"IIFE"),虽然是很久之前的技巧了,但是这篇文章讲的东西挺有意思。来自老外的博客,我大概翻译了一下,能帮助理解很多关于IIFE的细节
原文链接:benalman.com/news/2010/1…
原文作者:Ben Alman
为了防止你没注意到,提前说明一下,我是一个对术语有点执着的人,所以在听过很多次虽然流行但是具有误导性的JavaScript术语"self-executing anonymous function"(自执行匿名函数)之后,我最终决定把我的想法整理成一篇文章。
除了对该模式(译者注:立即执行函数表达式)到底是怎么运行的提供详尽的信息之外,实际上我还给出了一个关于进一步给这个模式命名的建议。如果你想跳过的话,你可以直接跳到立即调用函数表达式进行阅读,但是我推荐你看完整篇文章。
也请您理解,这篇文章并不是”为了证明你是对的我是错的“之类的东西,我是真的有兴趣帮助人们理解潜在的复杂概念。而且我察觉到,使用准确且一致的术语来促进理解,是人们能做到的最简单的事情之一。
所以,这一切到底是怎么回事呢?
在JavaScript中,每个函数被执行的时候,都会创建一个新的执行上下文。因为定义在一个函数内的变量和函数都只能在该上下文内访问,而不可以在该上下文外部访问,所以执行一个函数提供了一个可以非常容易的创建私有变量的方法。
//因为这个函数返回的另一个函数能够访问私有的`i`,所以这个被返回的函数实际上是“有特权的”
// Because this function returns another function that has access to the "private" var i, the returned function is, effectively, "privileged."
function makeCounter() {
//`i`只有在`makeCounter`内部可以访问
// `i` is only accessible inside `makeCounter`.
var i = 0;
return function() {
console.log( ++i );
};
}
//注意,counter和counter2各自拥有自己作用域的`i`
// Note that `counter` and `counter2` each have their own scoped `i`.
var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2
var counter2 = makeCounter();
counter2(); // logs: 1
counter2(); // logs: 2
i; // ReferenceError: i is not defined (it only exists inside makeCounter)
//`i`只存在于makeCounter内部
问题的关键
现在,无论你是像这样定义一个函数:function foo(){},还是像这样:var foo = function(){},你最终都只得到了一个函数标识符,你可以在函数标识符后边加上括号()来调用它,就像这样:foo()。
// 像这样定义的函数可以在函数名后边加一个()来调用它,就像foo()这样,而且因为foo只是一个对函数表达式function() { /* code */ }的引用
// Because a function defined like so can be invoked by putting () after the function name, like foo(), and because foo is just a reference to the function expression `function() { /* code */ }`...
var foo = function(){ /* code */ }
//那是不是说函数表达式是不是可以直接在后边加一个()来调用它呢?
// ...doesn't it stand to reason that the function expression itself can be invoked, just by putting () after it?
function(){ /* code */ }(); // SyntaxError: Unexpected token (
如你所见,错误被捕获了。无论是在全局作用域还是在一个函数内部,当解析器碰到function关键字的时候,解析器会认为这是一个函数声明(function declaration),而不是函数表达式(function expression)。默认情况下,如果你没有明确告诉解析器,让它去期待一个表达式,那么解析器会认为它看到的是一个没有名字的函数定义,并抛出一个SyntaxError异常,因为函数定义需要有一个名字。
题外话:函数,括号和SyntaxError
有趣的是,如果你给函数指定一个名称并且在函数后面立马加一个括号的话,尽管出于不同的原因,解析器仍会抛出一个SyntaxError。尽管表达式(expression)后的加上括号意味着表达式是一个可以被调用的函数,但是在声明(statement)后加一个括号,括号和声明是完全分开的,并且括号仅仅是一个分组运算符(用于控制优先级的方法)
//尽管在语法层面上这个函数声明是有效的,但它依然是一个声明,紧随其后的一组括号无效是因为分组操作符需要包含一个表达式
// While this function declaration is now syntactically valid, it's still a statement, and the following set of parens is invalid because the grouping operator needs to contain an expression.
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
//现在,你在括号内放一个表达式,错误不再抛出了,但是函数依然没有执行,理由如下:
// Now, if you put an expression in the parens, no exception is thrown, but the function isn't executed either, because this:
function foo(){ /* code */ }( 1 );
// 上面的代码等价于下面的代码:一个函数定义跟着一个毫不相关的表达式
// Is really just equivalent to this, a function declaration followed by a completely unrelated expression:
function foo(){ /* code */ }
( 1 );
你可以通过Dmitry A. Soshnikov的这篇文章了解更多,ECMA-262-3 in detail. Chapter 5. Functions.
立即执行函数表达式(IIFE)
幸运的是,修复这个SyntaxError异常很容易。为了让解析器去期待一个函数表达式,最广为人知的方法是直接把函数用()包裹起来,因为括号中无法包含声明(statement)。此时,当解析器碰到function关键字的时候,它就会把括号内的内容解析为函数表达式而不是函数声明。
// 以下两种办法都可以立即执行一个函数表达式,利用函数的执行上下文来创建私有变量
// Either of the following two patterns can be used to immediately invoke a function expression, utilizing the function's execution context to create "privacy."
(function(){ /* code */ }()); // Crockford recommends this one
(function(){ /* code */ })(); // But this one works just as well
//因为括号和强制运算符都是为了消除函数表达式和函数定义之间的歧义,所以当解析器已经在期待一个表达式的时候他们可以被省略(请阅读下面的“重要提示”)
// Because the point of the parens or coercing operators is to disambiguate between function expressions and function declarations, they can be omitted when the parser already expects an expression (but please see the "important note" below).
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
//如果你不在乎返回值或者会你的代码有那么亿点点难阅读的话,你也可以省点内存,在函数前面加一个一元运算符来达成目的
// If you don't care about the return value, or the possibility of making your code slightly harder to read, you can save a byte by just prefixing the function with a unary operator.
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
//这是来自@kuvos的另外一种变体,用new关键字可以,尽管我不确定这是否会对性能有影响,
// Here's another variation, from @kuvos - I'm not sure of the performance implications, if any, of using the `new` keyword, but it works. http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
//如果需要传参数的话需要加括号
// Only need parens if passing arguments
new function(){ /* code */ }()
关于括号的重要说明
在函数表达式周围的额外的 "消歧义 "后缀是不必要的情况下(因为解析器期待一个表达式),作为一个惯例,在制定任务时使用它们仍然是一个好主意。这样的括号通常表示函数表达式将被立即调用,而变量将包含函数的结果,而不是函数本身。这可以为阅读你的代码的人省去麻烦,因为他们不得不向下滚动到可能是一个很长的函数表达式的底部,以查看它是否被调用。作为一条经验法则,虽然从技术上讲,编写不含糊的代码可能对防止JavaScript解析器抛出SyntaxError异常是必要的,但编写不含糊的代码对防止其他开发者向你抛出 "WTFError "异常也是相当必要的。
使用闭包保存状态
就像当函数被其命名标识符调用时可以传递参数一样,参数也可以传递给立即调用函数表达式。因为在另一个函数内部定义的任何函数都可以访问外部函数的传入参数和变量(也就是俗称的闭包),立即执行函数可用于“锁定”值并有效地保存状态。
如果您想了解有关闭包的更多信息,可以看下这个Closures explained with JavaScript
//这并不会和预想的一样工作,因为i的值没有被锁定,相反,当点击的时候每个链接都会alert elements的总数(当循环执行完),因为那就是i的最终值
// This doesn't work like you might think, because the value of `i` never
// gets locked in. Instead, every link, when clicked (well after the loop
// has finished executing), alerts the total number of elements, because
// that's what the value of `i` actually is at that point.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + i );
}, 'false' );
}
//这样就能正常工作,因为在IIFE内部,i的值被锁定为`lockedInIndex`,当循环结束执行,尽管i的值是elements的总数,但在IIFE的内部,`lockedInIndex`的值是当IIFE执行时i传给它的值,所以当链接被点击,会弹出正确的值
// This works, because inside the IIFE, the value of `i` is locked in as
// `lockedInIndex`. After the loop has finished executing, even though the
// value of `i` is the total number of elements, inside the IIFE the value
// of `lockedInIndex` is whatever the value passed into it (`i`) was when
// the function expression was invoked, so when a link is clicked, the
// correct value is alerted.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
(function( lockedInIndex ){
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
}, 'false' );
})( i );
}
//你也可以像这样使用IIFE,仅在Click的回调函数中使用IIFE,而非整个`addEventListener`,无论使用哪种方式,虽然两个示例都使用 IIFE,我觉得前面的例子更具可读性
// You could also use an IIFE like this, encompassing (and returning) only
// the click handler function, and not the entire `addEventListener`,
// assignment. Either way, while both examples lock in the value using an
// IIFE, I find the previous example to be more readable.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', (function( lockedInIndex ){
return function(e){
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
};
})( i ), 'false' );
}
请注意,在最后两个示例中,lockedIndex 写为i也毫无问题,但是使用不同名称的标识符作为函数参数会使该概念更容易解释。
立即调用函数表达式最有利的副作用之一是,由于此未命名或匿名的函数表达式是立即调用的,而无需使用标识符,因此可以在不污染当前作用域的情况下使用闭包。
"Self-executing anonymous function(自执行匿名函数)"有什么问题吗?
你已经看到它提到过几次,但是如果不清楚,我建议使用术语“Immediately-Invoked Function Expression(立即执行函数表达式)”,如果你喜欢首字母缩略词,则建议使用“IIFE”。有人向我建议它的发音为“iffy”,我喜欢它,所以我们用它继续。
什么是Immediately-Invoked Function Expression(立即执行函数表达式)?它是一个被立即调用的函数表达式。就像它的名字所让你相信的那样。
我希望看到JavaScript社区成员在他们的文章和演讲中采用 "Immediately-Invoked Function Expression(立即调用函数表达式)"和 "IIFE "这个术语,因为我觉得它使得理解这个概念更容易一些,而且 "self-executing anonymous function(自执行匿名函数)"这个术语甚至不是真的准确
//这是一个自执行函数,这是一个递归执行自己的函数
// This is a self-executing function. It's a function that executes (or
// invokes) itself, recursively:
function foo() { foo(); }
//这是一个自执行匿名函数,因为他没有标识符,所以它必须使用arguments.callee(指定当前正在执行的函数)来执行它自己
// This is a self-executing anonymous function. Because it has no
// identifier, it must use the the `arguments.callee` property (which
// specifies the currently executing function) to execute itself.
var foo = function() { arguments.callee(); };
//当foo标识符引用这个函数的时候,它有可能是一个自执行匿名函数,但是如果你改变了foo,让它指向别的东西,那么你就得到了一个“曾经自执行的”匿名函数
// This *might* be a self-executing anonymous function, but only while the
// `foo` identifier actually references it. If you were to change `foo` to
// something else, you'd have a "used-to-self-execute" anonymous function.
var foo = function() { foo(); };
//有的人管这个叫自执行的匿名函数,尽管他根本没执行他自己,它仅仅是立即执行了而已
// Some people call this a "self-executing anonymous function" even though
// it's not self-executing, because it doesn't invoke itself. It is
// immediately invoked, however.
(function(){ /* code */ }());
//给函数表达式加一个标识符(因此创造了一个命名的函数表达式),这在debugger的时候非常有用,但是一旦命名,函数就不再匿名了
// Adding an identifier to a function expression (thus creating a named
// function expression) can be extremely helpful when debugging. Once named,
// however, the function is no longer anonymous.
(function foo(){ /* code */ }());
//IIFE可以自执行,尽管这可能不是最常用的方案
// IIFEs can also be self-executing, although this is, perhaps, not the most
// useful pattern.
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());
//最后要说的一件事,在BlackBerry 5上会造成一个错误,因为在一个命名了的函数表达式内部,该函数的名字是undefined,很神奇对吧?
// One last thing to note: this will cause an error in BlackBerry 5, because
// inside a named function expression, that name is undefined. Awesome, huh?
(function foo(){ foo(); }());
希望这些例子能够说明,"自执行(self-executing)"这个词有些误导性,因为不是函数自己在执行,尽管函数正在被执行。另外,"匿名 "是不必要的特指,因为一个立即调用的函数表达式可以是匿名的,也可以是命名的。至于我喜欢 "调用(invoked) "而不是 "执行(executed)",这只是一个简单的押韵问题;我认为 "IIFE "看起来和听起来都比 "IEFE "好。
以上这些就是我的“高见”
有趣的是:由于arguments.callee在ECMAScript 5严格模式下被废弃了,所以在ECMAScript 5严格模式下创建一个 "自执行匿名函数 "在技术上是不可能的。
最后的最后:模块(The Module Pattern)
当提到函数表达式时,如果我不提到模块模式,那就有点不负责任了。如果你不熟悉JavaScript中的模块模式,它与我的第一个例子类似,但返回的是一个对象而不是一个函数(而且通常以单例的形式实现,就像下面这个例子一样)。
//创建一个会立即执行的匿名函数表达式,把它的返回值赋给变量,这个办法省掉了`makeWhatever`的函数引用
// Create an anonymous function expression that gets invoked immediately,
// and assign its *return value* to a variable. This approach "cuts out the
// middleman" of the named `makeWhatever` function reference.
//
//正如前边的重要说明提到的,尽管这个函数表达式周围可以不写括号,但为了方便看出来函数的值被赋给了这个变量而不是引用了函数本身
// As explained in the above "important note," even though parens are not
// required around this function expression, they should still be used as a
// matter of convention to help clarify that the variable is being set to
// the function's *result* and not the function itself.
var counter = (function(){
var i = 0;
return {
get: function(){
return i;
},
set: function( val ){
i = val;
},
increment: function() {
return ++i;
}
};
}());
//`counter`是一个有带有属性的对象,在这里这些属性恰好是方法
// `counter` is an object with properties, which in this case happen to be
// methods.
counter.get(); // 0
counter.set( 3 );
counter.increment(); // 4
counter.increment(); // 5
counter.i; // undefined (`i` is not a property of the returned object)
//undefined(i不是被返回对象的属性)
i; // ReferenceError: i is not defined (it only exists inside the closure)
//i仅在闭包中存在
模块模式不仅令人难以置信的强大,而且令人难以置信的简单。只需很少的代码,你就可以有效地使用命名空间来包含相关的方法和属性,以一种最小化全局范围污染和创建隐私的方式来组织整个模块的代码。