http://benalman.com/news/2010/11/immediately-invoked-function-expression/
如果你没有注意到,我对术语有一点点坚持。 所以,在听到流行但误导性的JavaScript术语“自我执行的匿名函数”[self-executing anonymous function](或自我调用的匿名函数[self-invoking anonymous function])之后,我终于决定将我的想法整理成一篇文章。
除了提供关于这种模式如何实际工作的一些非常全面的信息之外,我实际上已经就我们应该称之为什么提出了建议,并继续前进。 另外,如果您想跳过,可以查看一些实际的立即调用函数表达式,但我建议阅读整篇文章。
请理解,本文不打算成为“我是对的,你错了”的一类东西。 我真的有兴趣帮助人们理解潜在复杂的概念,并认为使用一致和准确的术语是人们可以做的最容易理解的事情之一。
那么,这究竟是什么呢?
在JavaScript中,每个函数在被调用时会创建一个新的执行上下文。 因为函数中定义的变量和函数只能在该上下文中访问,而不能在该上下文之外访问,所以调用函数提供了一种创建隐私的非常简单的方法。
// Because this function returns another function that has access to the // "private" var i, the returned function is, effectively, "privileged." //因为这个函数返回可以访问“private” var i的另一个函数,所以返回的函数实际上是“特权的”。 function makeCounter() { // `i` is only accessible inside `makeCounter`. //`i`只能在`makeCounter`中访问。 var i = 0; return function() { console.log( ++i ); }; } // Note that `counter` and `counter2` each have their own scoped `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 is not defined (it only exists inside makeCounter)28 // ReferenceError:i 没有定义(它只存在于makeCounter中)
在许多情况下,无论makeWhatever(译者注:原文是makeWhatever,但是上文未提到)函数返回什么,都不需要多个“实例”,并且可以只处理单个实例,而在其他情况下,甚至不显式返回值。
事件的核心
现在,无论您是定义函数foo(){}还是var foo = function(){},您最终得到的是一个函数的标识符,您可以在它后面加上parens(括号,()),比如foo()。
// 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 */ }`... // 因为像这样定义的函数可以通过在函数名后加上()来调用,比如foo(),并且因为foo只是对函数表达式[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 (15 //SyntaxError:意外的标记(
正如你所看到的,有一个问题。 当解析器在全局范围内或函数内部遇到function关键字时,默认情况下它将其视为函数声明[function declarasion](语句),而不是函数表达式。 如果您没有明确告诉解析器期望表达式,它会看到它认为是没有名称的函数声明,并抛出SyntaxError异常,因为函数声明需要一个名称。
另外:函数,parens(括号)和SyntaxErrors
有趣的是,如果您要为该函数指定一个名称并将parens立即放在后面,那么解析器也会抛出一个SyntaxError,但出于不同的原因。 虽然放置在表达式后面的parens表明该表达式是要调用的函数,但放置在语句后的parens与前面的语句完全分开,并且仅仅是一个分组操作符(用作控制评估优先级的手段)。
// 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. // 虽然这个函数声明现在在语法上是有效的,但它仍然是一个声明,而下面的一组parens是无效的,因为分组操作符需要包含一个表达式。 function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) //SyntaxError:意外的标记 ) // Now, if you put an expression in the parens, no exception is thrown... // but the function isn't executed either, because this: //现在,如果你在parens中放置一个表达式,不会抛出异常......但是函数也不会执行,因为: 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(第5章 函数)中详细了解这方面的内容。
立即调用函数表达式(IIFE [Immediately-Invoked Function Expression])
幸运的是,SyntaxError“修复”很简单。 告诉解析器预期函数表达式的最普遍接受的方式就是将其包装在parens中,因为在JavaScript中,parens不能包含语句。 此时,当解析器遇到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." //可以使用以下两种模式中的任何一种立即调用函数表达式,利用函数的执行上下文来创建“privacy”。 (function(){ /* code */ }()); // Crockford recommends this one // Crockford建议这一个 (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). // 因为parens或强制操作符的目的是消除函数表达式和函数声明之间的歧义,当解析器已经获得期望的表达式时,可以省略它们(但是请参阅下面的“重要提示”)。 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 */ }(); // 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 //以下是另一个变体,来自@kuvos--我不确定使用`new`关键字的性能影响(如果有的话),但它有效。http://twitter.com/kuvos/status/18209252090847232 new function(){ /* code */ } new function(){ /* code */ }() // Only need parens if passing arguments40 // 如果传递参数只需要parens
关于那些parens的重要说明
在不需要额外的“消除歧义”元素的情况下(因为解析器已经期望得到表达式),在进行赋值时使用它们仍然是一个好主意,这是一种约定。
这样的元素通常表示将立即调用函数表达式,变量将包含函数的结果,而不是函数本身。 这可以使读取代码的人省去向下滚动到可能是很长的函数表达式的底部以查看它是否被调用的麻烦。
作为一个经验法则,虽然编写明确的代码可能在技术上有必要保持JavaScript解析器不会抛出SyntaxError异常,但编写明确的代码对于防止其他开发人员在您处抛出“WTFError”异常也是非常必要的!
使用闭包保存状态
就像当函数被它们的命名标识符调用时可以传递参数一样,当立即调用函数表达式时也可以传递参数。由于在另一个函数中定义的任何函数都可以访问外部函数的传入参数和变量(这种关系称为闭包),因此可以使用一个立即调用的函数表达式来“锁定”值并有效地保存状态。
如果您想了解更多关于闭包的知识,请阅读用JavaScript解释的闭包。
// 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. //这不会像你想象的那样工作,因为`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' ); } // 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. // 这是有效的,因为在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 ); } // 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. //您也可以像这样使用IIFE,仅包含(并返回)click处理函数,而不是整个`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' ); }
请注意,在最后两个示例中,lockedInIndex可能刚好在没有任何问题的情况下被调用,但使用不同名称的标识符作为函数参数使得该概念更容易解释。
立即调用函数表达式的一个最有利的副作用是,因为这个未命名的或匿名的函数表达式被立即调用,而不使用标识符,所以可以使用闭包而不污染当前范围。
“自执行匿名函数”有什么问题?
您已经见过它多次提到,但如果不清楚,我建议使用术语“立即调用函数表达式”和“IIFE”(如果您喜欢首字母缩略词)。“iffy”的发音是我提出来的,我很喜欢这个发音,我们继续。
什么是立即调用函数表达式? 这是一个立即被调用的函数表达式。 就像这个名字会让你相信。
我希望看到JavaScript社区成员在他们的文章和演示中采用“立即调用函数表达式”和“IIFE”这两个术语,因为我觉得这样理解这个概念更容易一些,而且因为“自执行匿名函数”这个术语并不准确:
// This is a self-executing function. It's a function that executes (or // invokes) itself, recursively: // 这是一个自我执行的功能。 这是一个递归执行(或调用)自身的函数: function foo() { foo(); } // 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. //这是一个自我执行的匿名函数。 因为它没有标识符,所以它必须使用`arguments.callee`属性(它指定当前正在执行的函数)来执行它自己。 var foo = function() { arguments.callee(); }; // 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. //这'可能'是一个自我执行的匿名函数,但只有在`foo`标识符实际引用它时。 如果你想把`foo`改成别的东西,你会有一个“二手自行执行”的匿名函数。 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 */ }()); // 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 */ }()); // IIFEs can also be self-executing, although this is, perhaps, not the most // useful pattern. // IIFE也可以自我执行,尽管这也许不是最有用的模式。 (function(){ arguments.callee(); }()); (function foo(){ foo(); }()); // 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? // 最后要注意的是:这会导致BlackBerry 5出现错误,因为在命名函数表达式中,该名称是未定义的。 真棒,嗯? (function foo(){ foo(); }());
希望这些例子清楚地表明,“自我执行”这个术语有些误导,因为它不是执行自身的函数,即使函数正在执行。 另外,“匿名”是不必要的,因为立即调用的函数表达式可以是匿名的或者是命名的。 至于我更喜欢“invoked”而非“executed”,这是一个简单的头韵问题; 我认为“IIFE”看起来和听起来比“IEFE”好。
就是这样了。 这是我的想法。
有趣的事实:因为arguments.callee在ECMAScript 5严格模式中被弃用,实际上在ECMAScript 5严格模式下创建“自我执行的匿名函数”在技术上是不可能的。
最后一点:模块模式
在我调用函数表达式的时候,如果我至少没有提到模块模式,那我就会失职。 如果您对JavaScript中的模块模式不熟悉,它与我的第一个示例类似,但返回的是Object而不是Function(并且通常以单例实现,如本例中所示)。
// 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. //创建一个立即被调用的匿名函数表达式,并将其*返回值*赋给一个变量。 这种方法“切断了名为`makeWhatever`(译者注:这个并不存在于上下文)函数引用的中间人"。 //正如上面“重要说明”中所解释的,尽管在这个函数表达式周围不需要使用parens,但仍然应该将它们作为约定使用,以帮助阐明变量被设置为函数的*result*,而不是函数本身。 var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function( val ){ i = val; }, increment: function() { return ++i; } }; }()); // `counter` is an object with properties, which in this case happen to be // methods. // ' counter '是一个具有属性的对象,在本例中正好是方法。 counter.get(); counter.set( 3 ); counter.increment(); counter.increment(); 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)44 // ReferenceError:没有定义i(它只存在于闭包内)
模块模式方法不仅非常强大,而且非常简单。使用很少的代码,您可以有效地命名相关方法和属性的名称空间,以一种最小化全局范围污染和创建隐私的方式组织整个代码模块。
进一步阅读
希望这篇文章是内容丰富的,并回答了你的一些问题。 当然,如果你现在有更多的问题,那么你可以通过阅读下面的文章来学习更多关于函数和模块模式的知识。
ECMA-262-3的详细信息 第5章 函数 - 德米特里·Soshnikov
函数和函数范围 - Mozilla开发者网络
命名的函数表达式- Juriy“kangax”Zaytsev
JavaScript模块模式:深入 - Ben Cherry
用JavaScript解释闭包 - 尼克摩根
我想感谢Asen Bozhilov和John David Dalton提供技术建议,以及Nick Morgan对他的见解。 如果您还想贡献,请在评论中发布任何建议或反馈,谢谢!