写在前面
- 因为最近在学习模块化编程,而模块化编程,就是为了解决污染全局变量的问题。我想应该都是通过立即调用函数表达式实现的吧。
- 这是一篇译文,原文:Immediately-Invoked Function Expression (IIFE)
下文中提到的 IIFE 其实就是“立即调用函数表达式”
为什么需要 IIFE
- 在 JavaScript 中,每一次调用 函数 都会创建一个可执行上下文。这样,在函数内部定义的变量或者函数只能在内部访问,而不能被外部访问。在该上下文中,为调用函数提供了非常简单的方法来保持私有性。
// 因为该函数返回的是另一个有权访问私有变量 i 的函数,
// 所以实际上返回的函数是一种“特权”
function makeCounter() {
// `i` 只能在 `makeCounter`内部访问.
var i = 0;
return function() {
console.log( ++i );
};
}
// `counter` and `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)
- 大多数情况下,你不需要该函数的多个实例,只想要单例模式,或者某些情况你都不关心返回值。
核心
- 不管你是使用 function foo(){} 还是使用 var foo = function(){},你只是给函数取了个标识符,为了后面用 () 调用,像这样foo()。
// 这样定义的函数可以像 foo() 这样调用( 在函数名后面加一对() )。
// 因为 foo 只是函数表达式 `function(){/* code */}`的引用。
var foo = function(){ /* code */ }
// 那是不是也就是说在函数表达式后面加一对 () 就能调用它自己了?
function(){ /* code */ }(); // SyntaxError: Unexpected token (
- 如你所见,捕获了一个异常。当解析器在全局范围内或函数内遇到 function 关键字时,它默认将其视为函数声明(语句),而不是函数表达式。函数声明和函数表达式的理解
- 如果你没有明确告诉解析器这是表达式,它会认为这是一个没有名称的函数声明,并抛出SyntaxError异常,因为函数声明需要名字
函数,括号 和 语法错误
- 有趣的是,如果你指定函数名,然后在后面加一对 (),解析器一样会抛出另一个语法异常。()放在表达式后面意味着这个表达式是个被调用的函数,而 () 放在语句的后面,完全就是为了和前面的语句进行区分,只是个简单的分组操作符(控制优先级的手段)。
// 虽然该函数声明在语法上有效,但仍然是条语句,后面的一对 () 是无效的,
// 因为分组操作符需要包含表达式
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
// 如果你在后面的 () 中放一个表达式就没异常了。。。
// 但是函数也不会执行,因为这条语句:
function foo(){ /* code */ }( 1 );
// 跟这条语句完全一样, 函数声明后面紧跟一个完全不相干的表达式
function foo(){ /* code */ }
(1);
你可以在这里了解更多有关函数的细节。
立即调用函数表达式 (IIFE)
- 幸运的是,语法错误修复起来很简单。明确告诉解析器这里需要的是表达式的一种方法,就是用 () 包裹起来, 因为在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 Function的方式使用立即调用函数表达式
new Function('console.log("hi")')()
// 你需要把参数名、函数体都作为参数传给 Function 的构造函数,在后面的括号里写上实参,表示调用传值
new Function("a", "b", 'console.log(a + b)')(1, 2)
关于括号的重要说明
-
如果有关函数表达式的“消歧”问题是不必要的(因为解析器已经明确知道这里需要函数表达式),那么在进行赋值时使用 () 仍然是个好主意,这是一种惯例。
-
这样的 () 通常表示将立即调用函数表达式,并且返回的变量包含函数的结果,而不是函数本身。这在阅读一段很长的代码时是很方便的,不用滚到最后面就已经知道它是不是已经被调用了。
-
根据经验,编写明确的代码以防止JavaScript解析器抛出SyntaxError异常,不仅在技术上是必要的,为了防止其他开发人员抛出“WTFError”异常!也是非常必要的!
用闭包保存状态
- 就像通过命名标识符调用函数时可以传递参数一样,也可以在立即调用函数表达式时传递它们。并且,因为在函数内定义的任何函数都可以访问这个函数传入的参数和变量(这种关系称为闭包),所以可以使用立即调用函数表达式来“锁定”参数值并有效地保存状态。
- 如果你想了解更多,你可以阅读闭包
// 这并没有像你想的那样运行,因为 i 的值没有被锁定。
// 相反的,每一次点击 link ,
// 都会弹出元素的总数量(循环完全结束时的值),因为那才是 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 的值是总的元素数量,而 lockedInIndex 是函数调用时传进来的 i 的值。
// 所以,当点击 link 时,就会弹出正确的值。
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' );
}
- 仔细观察最后两个例子,lockedInIndex写成i也没问题,因为两者的作用域不同。但是用不同的标识符作为函数参数会使概念更容易解释。
- IIFE 另一个最大的好处就是因为未命名的或者匿名的函数表达式是在不使用标识符的情况下就立即调用的,所以可以使用闭包而不污染当前作用域。
“自执行匿名函数”有什么问题?
-
而这篇文章是 2010年写的,可能当时这个叫法很难区分和递归的不同,所以作者才写了这篇文章,也可能是作者的功劳,现在都叫 IIFE了。
-
什么是立即调用函数表达式?就是一种立即调用的函数表达式
-
我希望 JavaScript 社区成员能采用 “立即调用函数表达式”或者“IIFE”这个术语。因为它更容易理解,而“自执行匿名函数”并不准确:
// 自执行函数。递归执行或调用:
function foo() { foo(); }
// 自执行匿名函数。
// 因为没有标识符,必需使用`arguments.callee`属性(指向当前执行的函数)执行自身
var foo = function() { arguments.callee(); };
// 这可能是自执行匿名函数,但只有在`foo`标识符实际引用时才是。
// 如果你改变`foo`的引用,那你只是曾经拥有一个自执行匿名函数。
var foo = function() { foo(); };
// 有人把这也叫做自动执行匿名函数,即使它并不是自动执行。
// 因为它没有调用它自己。它只是立即执行。
(function(){ /* code */ }());
// 为函数表达式添加标识符(创建了一个具名函数表达式)在调试时会非常有用。
// 一旦命名,函数就不在匿名。
(function foo(){ /* code */ }());
// IIFE 也可以是自动执行的,尽管这不是最有用的模式。
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());
// 最后一件需要注意的是:在 BlackBerry 5 可能会出错。
// 因为在具名函数表达式中,函数表达式的名称是 undefined 。
(function foo(){ foo(); }());
- 希望这些例子能清楚的表明“自执行”这个术语有点诡异。因为即使函数正在执行,它也不是正在执行的函数。此外“匿名”是不必要特意指定的,因为立即调用的函数表达式可以是匿名或者命名的。
最后一块:模块
- 虽然b本文强调的立即调用函数表达式,但如果我都没提模块模式的话,那将是我的疏忽。如果你不熟悉 JavaScript 中的模块模式的话,也没关系,其实它就和我的第一个示例类似,但返回的是 Object 而不是 Function(并且通常实现为单例,如本例所示)。
// 创建一个立即调用的匿名函数
// 把返回值赋值给一个变量,这个变量包含你要暴露的属性(私有化)
// 这种方式移除了名为`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` is not a property of the returned object)
i; // ReferenceError: i is not defined (it only exists inside the closure)
- 模块模式的方法不仅非常强大,而且非常简单。使用非常少的代码,你就可以有效的命名空间相关的方法和属性,以最小化全局污染和保持私有的方式组织整个代码模块。
网友评论