这篇文章并没有打算说明:“我是对的,你是错的”之类观点。我只是希望帮助更多的人真正理解那些复杂的概念,并采用一致,准确的属于。和大家一起做一些最简单的事情来帮助大家理解相关概念。
在JavaScript中,每个函数被调用时都会创建一个全新的上下文环境。因此,在函数内部定义的变量和函数就只能在函数内部访问,在外部无法访问,那么在该上下文环境中,调用的函数就提供了一个非常方便的方式来创建私有成员。
// 因为返回的函数能够访问外部函数内部的"私有"变量i,返回的函数实际上有访问这个i的特权
function makeCounter() {
// 仅仅在makeCounter内部才能访问`i`
var i = 0;
return function() {
consolelog(++i);
};
}
// 注意下面的 counter 和 counter2 各自都有它们自己作用域内的 `i`
var counter = makeCounter();
counter(); // 1
counter(); // 2
var counter2 = makeCounter();
counter2(); // 1
counter2(); // 2
i; // 引用错误: i 没有定义 (i仅仅存在于 makeCounter 内部)
核心
定义一个诸如 function foo(){}
或者 var foo = function(){}
函数, 最终都会得到一个有标识的函数,然后可以在后面插入一对圆括号 () 来调用函数,就像 foo()。
// 因为以这种方式定义的函数可以通过在函数名后面插入一对 () 来调用它,就像 foo(),因为 foo 是一个指向函数表达式 `function() {/* code */}`的引用。
var foo = function() {/* code */}
// 那么是否在函数表达式后面插入 () 就能调用自身呢?
function() {/* code */}(); // 语法错误:意外的标识 (
可以看到这里捕获到了一个错误。当解析器在全局作用域或者函数的内部遇到 function 关键字时,默认情况下它会将它当作一个函数声明(语句),而不是一个函数表达式。如果不能明确的告诉解析器它是一个函数表达式,那么解析器就会认为它是一个没有命名的函数声明,并且抛出语法错误的异常,这是因为函数声明必须有一个名字。
函数,括号和语法错误
有趣的是,如果给函数指定一个名字并且立即在它后面插入一对括号,解析器也会抛出一个错误,但是这不同于前面的原因。因为放在一个表达式后面的括号表示该表达式是一个被调用的函数,而括号放在语句的后面时,它是完全独立于前面的语句的,此时它仅仅是一个分组操作符(作为一个控制执行优先级的操作符)。
// 虽然下面的函数声明语法是有效的,但它仍然是一个语句,但是接下来的括号是无效的,因为分组操作符必须包含一个表达式。
function foo() {/* code */}(); // 语法错误:意外的标识)
// 但是如果在括号中插入一个表达式,就不会抛出异常...,但是函数还是不会执行,就像这样:
function foo() {/* code */}(1);
// 这其实相当于一个函数声明后面跟了一个完全无关的表达式:
function foo() {/* code */}
(1);
You can read more about this in Dmitry A. Soshnikov’s highly informative article, ECMA-262-3 in detail. Chapter 5. Functions
立即调用函数表达式
幸运的是,前面的语法错误“修复”起来都很简单。最为广泛接受的方式是告诉将函函数表达式用一对括号包裹起来,在JavaScript中,括号中不可以包含语句。这样一来,当解析器遇到 function 关键字时,它会将它当作一个函数表达式而不是一个函数声明。
(function(){/* code */}());
(function(){/* code */})();
// 由于括号和强制运算符的作用是消除函数表达式和函数声明之间的歧义,因而解析器碰到正确的表达式时它们是可以省略的。
var i = function() { return 10; }();
true && function() {/* code */}();
0, function() {/* code */}();
// 如果不关心返回值,或者让代码稍微难以阅读,还可以在函数前面前置一个一个字节的一元运算符
!function(){/* code */}();
~function(){/* code */}();
-function(){/* code */}();
+function(){/* code */}();
// 这里还有另外一种变体形式,但是性能上可能有一些问题,使用 `new` 关键字也能正常工作
new function() {/* code */}
new function() {/* code */}(); // 仅仅在需要传递参数的时候使用括号
括号注意事项
在某些情况下,用于消除歧义的函数表达式的括号是不必要的(因为解析器能够解析到预期的表达式),出于习惯问题,在赋值的时候坚持使用是个好注意。
这些括号通常标识立即调用这些函数表达式,并且变量会取得函数的结果而不会是函数自身。但是也可能导致别人阅读你的代码时如果表达式很长的情况要从上往下滚动看它是否被调用了。
避免语法错误问题非常有必要,于自己于其他人都好。
使用闭包保存状态
就像通过命名标识符调用函数式传递参数一样,调用函数表达式是也可以传递参数给它们。由于任何定义在其他函数内部的函数都可以访问外部函数传递进来的参数和外部函数的变量(这种关系被称为闭包),一个立即调用函数表达式可以用“锁定”值,然后用来有效的保存状态。
http://skilldrick.co.uk/2011/04/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' );
}
// 但是下面的代码能够正常工作,因为立即调用函数表达式的内部 `i` 被锁定为 `lockedIndex`。在循环执行完成之后,尽管 `i` 的值仍然是元素的总数,但是在立即调用函数表达式内部的 `lockedIndex` 总是函数表达式被调用时传递给它的 `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 );
}
// 也可以像下面这样使用立即调用函数表达式,仅仅包括点击处理函数,而不是完整的 `addEventListener` 操作。这两种方式都使用了一个立即调用函数表达式来锁定当前 `i` 的值,但是相对而言前面的方式更有利于阅读。
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', (function(lockedIndex){
return function(e)(
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
};
})( i ), false);
}
注意,在这两个例子中,lockedIndex
直接调用i
是没有问题的,但是使用不同的标识符作为传递给函数的参数会让这个概念更明确。
这是立即调用函数表达式最有利的一个副作用,因为它没有名字,或者说是匿名的,函数表达式会立即调用,而无需使用一个标识符来调用,闭包可以用来有效的避免污染当前作用域。
“自执行匿名函数”有什么问题呢?
很多地方都有提到,但是我更倾向于“立即调用函数表达式”,或者缩写的“IIFE”。
那么什么是立即调用函数表达式呢?它就是一个一个会立即被调用的函数表达式。正如它的名字。
“自执行匿名函数”的表达事实上并不是很准确:
// 下面是一个自执行函数。它是一个以递归的方式执行(或者说调用)自己的函数
function foo() { foo(); }
// 下面是一个自执行的匿名函数。因为它没有明确的标识符,因而必须使用 `arguments.callee` 属性(用来指定当前执行的函数)来执行自身。
var foo = function() { arguments.callee(); };
// 下面 *可能* 是一个自执行匿名函数,但是仅仅是在 `foo` 标识符实际引用它的情况下。 如果 `foo` 被改变为其他东西,那么就成了一个“用于自执行”的匿名函数。
var foo = function() { foo(); };
// 有些人也称作下面的这种形式为“自执行匿名函数”,尽管它并不是自执行的,因为它并没有调用自身。它实际上是立即被调用。
(function(){/* code */}());
// 给函数表达式添加一个标识符(也就是创建一个命名函数表达式)非常有助于调试。一旦给它命名,那么函数就不再是匿名的啦。
(function foo(){/* code */}());
// 立即调用自身函数也可以自执行,尽管可以这样,也许这并不是最有用的模式。
(function() { arguments.callee(); }());
(function foo() { foo(); }());
// 最后注意下面的代码在 BlackBerry 5 中会报错,因为在命名函数内部,该名称是未定义的。
(function foo() { foo(); }());
希望这些例子能够明确的表达“自执行”的术语是有误导性的,因为这些函数并不一定是执行自身,尽管它们的确是执行函数。同时“匿名”并不是必要的,立即调用函数表达式可以是匿名或者命名的。
arguments.callee 在ECMAScript 5的严格模式中已经废弃了, 实际上在 ECMAscript 5 严格模式中它无法创建一个“自执行匿名函数”。
最后的福利:模块模式
提到调用函数表达式时没有提到模块模式便是我的失职。 如果你不熟悉 JavaScript 中的模块模式,不要慌,它类似于第一个例子,但不同的是模块模式返回的时一个对象,而不是一个函数(通常实现为一个单例)。
// 首先创建一个匿名函数表达式然后立即调用, 并将这个函数的返回值赋值给一个变量。这种方式避免了使用一个名为 `makeWhatever` 的中间函数的引用
// 正如前面提到的“注意事项”,尽管包裹函数表达式的括号不是必须的,但仍然应该使用,用以约定赋给变量的值是函数的返回结果,而不是函数本身。
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; // 引用错误:i 没有定义
// 实际上 i 只存在于闭包的内部
模块模式不仅非常令人难以置信的强大,还非常的简单。很少的代码就可以有效的使用命名空间来维护相关的方法和属性,使用模块组织代码不但有效的避免了全局作用域污染,还创建了私有的特性。
参考阅读
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
原文:http://benalman.com/news/2010/11/immediately-invoked-function-expression/