翻译
原文地址
在听到对流行的javascript术语“self-executing anonymous function” (或者self-invoked anonymous function)的许多误导性的理解之后,我决定把我的想法组织成一篇文章。
在本文中,我主要描述了这种模式是如何工作的,以及我们应该如何正确使用这个模式。此外,如果你想快速阅读,你可以只看这些Immediately-Invoked Function Expressions,但是我建议你通过全文。
请注意,本文不是“我对,你错”的东西,我的目的是帮助人们了解复杂的概念。我觉得使用理解一致和准确的术语是人们相互理解的基础。
So, what’s this all about, anyways?
js中每个函数在被调用时,都会创建一个新的执行上下文。函数中定义的变量只能在这个函数的上下文中访问,而不能在函数外访问。因此使用函数能提供一种非常简单的保护隐私的方法。
// makeCounter函数返回了另外一个函数,而这个函数访问了私有变量`i`,因此这个返回函数实际上变成了私有的。
function makeCounter() {
// i 只能在makeCounter函数内部访问
var i = 0;
return function() {
console.log( ++i );
};
}
// 注意: `counter` 和 `counter2` 分别有他们自己作用域的 `i` 变量.
var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2
var counter2 = makeCounter();
counter2(); // logs: 1
counter2(); // logs: 2
i; // ReferenceError: i未定义(它只存在于makeCounter内部)
The heart of the matter
你定义一个函数,如函数foo(){}
或 var foo = function(){}
,得到的是一个函数的标识符,可以通过标识符+()
的形式调用它,如:foo()
。
// 可以通过“标识符+()”的方式调用函数,如:foo()。其中foo是一个指向函数表达式 `function() { /* code */ }`的引用
var foo = function(){ /* code */ }
// 那么能不能在函数表达式后面直接添加()来调用它呢?
function(){ /* code */ }(); // SyntaxError: Unexpected token (
正如你所见,有一个catch。 JS解析器在全局作用域或函数内部遇到中的function
关键字时,默认情况下,将其视为函数声明(语句),而不是函数表达式。 如果你没有明确告诉解析器期望一个表达式,JS解析器认为它看到的是一个没有名字的函数声明,从而抛出一个SyntaxError异常(因为函数声明需要一个名字)。
An aside: functions, parens, and SyntaxErrors
如果你为函数指定名称,并在其后放置括号,则JS解析器也会抛出一个SyntaxError。这个错误是因为:表达式之后的括号表示表达式是要调用的函数,而放在语句之后的括号与前面的语法完全分开,并且只是一个分组运算符(用作控制评估优先级的手段)。
// 虽然这个函数声明现在语法上有效,但它仍然是一个语句, 但是后面的一组括号是无效的,因为
// 分组运算符需要包含一个表达式.
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
// 如果你在后面的括号中放入表达式,没有异常会抛出,但是这个函数也不会执行,因为下面这个表达式
function foo(){ /* code */ }( 1 );
// 实际上只是等效于这个,一个函数声明后面跟一个完全不相关的表达式,如下:
function foo(){ /* code */ }
( 1 );
你可以在Dmitry A. Soshnikov的文章ECMA-262-3中详细了解这一点。
Immediately-Invoked Function Expression (IIFE)(立即执行函数)
幸运的是,这个SyntaxError很容易“fix”。通过将函数表达式包装在括号中就能解决,因为在JavaScript中,括号不能包含语句,当解析器遇到function
关键字时,它知道将其解析为函数表达式而不是函数声明。
// 可以使用以下两种模式中的任一种立即调用函数表达式,利用函数的执行上下文来创建“隐私”。
(function(){ /* code */ }()); // Crockford建议采用这一个
(function(){ /* code */ })(); // 同样可以工作
// 因为括号或强制运算符的作用是消除函数表达式和函数声明之间的歧义,当解析器已确定表达式时,
// 可以省略它们(但请参阅下面的“重要注释”)。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 如果你不关心返回值或者代码的可读性,你通过给函数添加一元运算符前缀来保存一个字节
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
// 这里是另一个变化,我不知道性能的影响,如果有`new`关键字,也可以使用,参考:
// http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
new function(){ /* code */ }() // Only need parens if passing arguments
An important note about those parens
如果不需要围绕函数表达式“消除歧义”的括号(因为解析器已经推导出这是一个表达式),那么在进行赋值时就使用它们是个好主意。这样的括号通常表示函数表达式将被立即调用,变量包含的是函数的结果,而不是函数本身。 这可以节省读取代码的麻烦,否则不得不下滚到可能是一个非常长的函数表达式的底部,以查看它是否已被调用。
Saving state with closures
立即调用函数表达式(IIFE)也能传入参数。因为函数内定义的任何函数可以访问外部函数的传入参数和变量(闭包),所以立即调用的函数表达式(IIFE)可以用于“锁定”值并有效地保存状态。
可以从Closures explained with JavaScript一文中了解更多关于闭包的知识。
// 下面这段代码并不执行,因为变量'i'并没有被锁定。相反,因为'i'的值实际上是在那一点,
// 所以每个连接被点击的时候会警告元素的总数。
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'的值是元素的总数,在IIFE中参数lockedInIndex的值是在被调用的时候传入的'i'的值,所以
// 当点击链接的时候,会弹出正确的值
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,包含(并返回)只有点击处理的函数,而不是对整个
// addEventListener方法赋值,无论那种方式都是使用IIFE锁定值,前面的一种方式代码可读性更好。
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' );
}
立即调用函数表达式(IIFE)的最有利的副作用之一是:匿名的函数表达式被立即调用,所以可以使用闭包而不污染当前范围。
What’s wrong with “Self-executing anonymous function?”
什么是立即调用的函数表达式? 它是一个立即调用的函数表达式。
我建议到JavaScript社区成员在他们的文章和演示文稿中采用术语“立即调用函数表达式”和“IIFE”,因为我认为它使理解这个概念更容易一些,而术语“self-executing anonymous function“不是真的甚至准确。
// 这是一个自执行功能。 它是一个以递归方式执行(或调用)自身的函数:
function foo() { foo(); }
// 这是一个自动执行的匿名函数。 因为它没有标识符,它必须使用`arguments.callee`来执行自身。
var foo = function() { arguments.callee(); };
// 这个可能是一个自动执行的匿名函数,但只有当`foo`标识符实际引用它。如果你把`foo`改成别的东西,
// 你会有一个“used-to-self-execute”的匿名函数。
var foo = function() { foo(); };
// 有些人称之为“自执行匿名函数”,即使它不是自执行的,因为它不会调用自身,但它会立即被调用。
(function(){ /* code */ }());
// 向表达式添加标识符(从而创建一个命名的函数表达式)在调试时非常有用。
// 然而,一旦命名,该函数不再是匿名的。
(function foo(){ /* code */ }());
// IIFE也可以是自动执行的,尽管这可能不是最有用的模式。
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());
// 最后一点要注意:这将导致BlackBerry 5中的错误,因为在命名的函数表达式中,该名称是未定义的。
(function foo(){ foo(); }());
希望这些例子清楚地表明,“自执行”一词有点误导,因为它不是执行自己的函数,即使函数正在执行。 另外,“匿名”是不必要的特定的,因为立即调用的函数表达式可以是匿名的或命名的。 至于我喜欢“调用”而不是“执行”,这是一个简单的事情; 我认为“IIFE”看起来和听起来比“IEFE”更好。
有趣的事实:因为arguments.callee在ECMAScript 5严格模式中已被弃用,在ECMAScript 5严格模式下创建一个“自执行匿名函数”实际上是不可能的。
A final aside: The Module Pattern
我希望在使用函数表达式时提到模块模式。如果你不熟悉JavaScript中的模块模式,它类似于我的第一个例子,但是返回一个Object而不是一个函数(并且通常实现为一个单例,如在这个例子中)。
// 创建一个立即调用的匿名函数表达式,并将其返回值赋给一个变量。 这种方法“切断了中间人”
// 命名的`makeWhatever`函数引用。正如上面的“重要说明”中所解释,尽管在这个函数表
// 达式周围不需要括号,但它们仍然应该作为约定使用,以帮助阐明变量被设置为函数的result
// 而不是函数本身。
var counter = (function(){
var i = 0;
return {
get: function(){
return i;
},
set: function( val ){
i = val;
},
increment: function() {
return ++i;
}
};
}());
// `counter'是一个具有属性的对象,在这种情况下恰好是方法。
counter.get(); // 0
counter.set( 3 );
counter.increment(); // 4
counter.increment(); // 5
counter.i; // undefined (`i` 不是返回对象的属性)
i; // ReferenceError: i 未定义 (只在闭包中存在)
模块模式方法既强大又简单。使用非常少的代码,您可以有效地命名空间相关的方法和属性,以最小化全局范围污染和创建隐私的方式组织整个代码模块。
Further reading
希望这篇文章能回答了你的一些问题。 当然,如果你现在有更多的问题,你可以通过阅读下面的文章更多地了解函数和模块模式。
- ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov
- Functions and function scope - Mozilla Developer Network
- Named function expressions - Juriy “kangax” Zaytsev
- JavaScript Module Pattern: In-Depth - Ben Cherry
- Closures explained with JavaScript - Nick Morgan