立即执行函数表达式(IIFE)

立即执行函数表达式(IIFE)

原文:Immediately-Invoked Function Expression (IIFE) by Ben Alman

以防你没有注意到,(事先声明)我是一个对于专业术语有些坚持的人。所以,当我多次听到流行却易产生误解的术语「自执行匿名函数」,最终决定将的想法总结到这篇文章中。

除此之外,为了提供一些更深入更彻底的关于这种模式是如何工作的,事实上我还建议了我们应该怎样称呼这种模式。另外,如果你想跳过这里,你可以直接阅读立即调用函数表达式,但是我建议你读完整篇文章。

你需要知道的是,这篇文章不是想说「我是对了,你是错的」之类的。我发自真心地想帮助人们理解这有点儿复杂的概念,并且我认为使用前后一致的精确术语是有助于人们理解的最简单的方式之一。

所以,它到底是什么

在JavaScript里,每个函数,当被调用时,都会创建一个新的执行环境。因为在函数里定义的变量和函数只能在函数内部被访问,外部无法获取;这种情况下,调用函数提供了一个非常简单的方法创建私有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 因为这个函数的返回值是另一个能通过i访问私有变量的函数,(privileged)?
function makeCounter() {
//i只能从`makeConuter`内部访问
var i = 0;
return function(){
console.log(++i);
};
}
// `counter`和`counter2`都有自己作用域中的变量 `i`
var counter = makeCounter();
counter(); //1
counter(); //2

var counter2 = makeCounter();
counter2(); //1
counter2(); //2

// ReferenceError: i is not defined(它只存在于makeCounter里)

在许多情况下,你可能并不需要由makeWhatever返回的多个实例,使用单例模式就可以实现。或者在其他情况中,你甚至不需要返回值。

核心问题

现在,无论你以function foo(){}的方式,还是var foo = function(){}的方式声明函数,你都可以在后边加上括号(圆括号,())来调用它们,,比如foo();

1
2
3
4
5
// 像下面这样定义的函数可以通过在函数名后加一对括号进行调用,像这样`foo()`,因为foo只是函数表达式`function(){/* code */}`的一个引用变量;
var foo = function(){/* code */}

//那这说明函数表达式可以通过在它自己后面加上一对括号就可以自己调用自己了吗?
function(){ /* code */}();//SyntaxError: Unexpected token (

正如你所看到的,这里捕获了一个异常。当圆括号在全局环境或函数内部遇到function关键字,它会被默认当作一个函数声明,而不是函数表达式,如果你不明确的告诉圆括号它是一个表达式,它会将其当作没有名字的函数声明并且抛出一个语法错误,因为函数声明需要一个名字。

题外话:函数、圆括号和错误

有趣的是,如果你为一个函数指定一个名字并在它后面放一对圆括号,同样的也会抛出错误,但这次是因为另外一个原因。当圆括号放在一个函数表达式后面指明了这是一个被调用的函数,而圆括号放在一个声明后面便意味着完全的和前面的函数声明分开了,此时圆括号只是一个简单的代表一个(组?)运算符(用来控制运算优先级)。

1
2
3
4
5
6
7
8
9
10
// 然而函数声明语法上是无效的,它仍然是一个声明,紧跟着的圆括号是无效的,因为圆括号里需要包含表达式
function foo(){ /* code */ }();
//👆SyntaxError: Unexpected token

// 现在,你把一个表达式放在圆括号里,没有抛出错误...但是函数也并没有执行,因为:
function foo(){/* code */}(1)

// 它等同于如下,一个函数声明跟着一个完全没有关系的表达式:
function foo(){/* code */}
(1);

关于这个细节,你可以阅读Dmitry A. Soshnikov的文章:ECMA-262-3 in detail. Chapter 5. Functions

立即执行函数表达式(IIFE)

幸运的是,修正语法错误很简单。最流行的也最被接受的方法是将函数声明包裹在圆括号里来告诉语法分析器去表达一个函数表达式,因为在JavaScript里,圆括号不能包含声明。因为这点,当圆括号为了包裹函数碰上了 function关键词,它便知道将它作为一个函数表达式去解析而不是函数声明。注意理解这里的圆括号和上面的圆括号遇到函数时的表现是不一样的,也就是说。

  • 当圆括号出现在匿名函数的末尾想要调用函数时,它会默认将函数当成是函数声明。
  • 当圆括号包裹函数时,它会默认将函数作为表达式去解析,而不是函数声明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 这两种模式都可以被用来立即调用一个函数表达式,利用函数的执行来创造私有变量
    (function(){/* code */}());
    //👆Crockford recommends this one

    (function(){/* code */})();
    //👆But this one works just as well

    // 因为括号的作用就是为了消除函数表达式和函数声明之间的差异
    // 如果解释器能预料到这是一个表达式,括号可以被省略
    // 不过请参见下面的「重要笔记」
    var i = function(){return 10;}();
    true && function(){/*code*/}();
    0,function(){}();

    //如果你并不关心返回值,或者让你的代码尽可能的易读,你可以通过在你的函数前面带上一个一元操作符来存储字节
    !function(){/* code */}();
    ~function(){/* code */}();
    -function(){/* code */}();
    +function(){/* code */}();

    // 这里是另外一种方法
    // 我(原文作者)不清楚new方法是否会影响性能
    // 但它却是奏效,参见http://twitter.com/kuvos/status/18209252090847232

    new function(){ /* code */ }
    new function(){ /* code */ }()
    // 👆只有当传入参数时才需要加括号

关于括号的重要须知

在一些情况下,当围绕在函数表达式周围的额外用来消除歧义的括号是没必要的(因为这时候的括号已经将其作为一个表达式去表达了),但它被当做约定俗成习惯时,仍然是一个好主意。

这样的括号指明函数表达式将会被立即调用,并且变量将会储存函数的结果,而不是函数本身。当这是一个非常长的函数表达式时,这可以节约其他人阅读你代码的时间,不用滚到页面底部去看这个函数是否被调用。

作为规则,当你书写没有歧义的代码时候,有必要阻止JavaScript解释器抛出错误;这同样很公平,可以避免让其他开发者写出抛出异常WTFError的代码影响到你!

保存闭包的状态

就像当函数通过他们的名字被调用时,参数会被传递,而当函数表达式被立即调用时,参数也会被传递。一个立即调用的函数表达式可以用来锁定值并且有效的保存此时的状态,因为任何定义在一个函数内的函数都可以使用外面函数传递进来的参数和变量(这种关系被叫做闭包)

关于闭包的更多信息,参见 Closures explained with JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/ /它的运行原理可能并不像你想的那样,因为`i`的值从来没有被锁定。相反的,每个链接,当被点击时(for循经执行完毕之后),因此会弹出所有元素的总数(elems.length),因为这是`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);
}
// 2018-01-27 更,编者按,,简单点说,因为在ES5中,只有(用var声明的变量)在函数中才有作用域。所以在for循环中用变量i属于for循环外,而不是for循环的{}中。同时,for循环又是一个同步事件,循环体中的监听事件是异步事件,异步事件处于 event loop 中,要等到所有同步事件执行完成之后,才会执行event loop 中的事件,,而那个时候,i 已然等于elems.length了。

// 而像下面这样改写就可以了,,因为在IIFE里,`i`值被锁定在了`lockedInIndex`里。在循环执行结束后,尽管`i`值的数值是所有元素的总和(elems.length),但每一次函数表达式被调用时,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);
// 2018-01-27 更,编者按,,在上边我们说了,在javascript中只有函数才有作用域,所以,,要想把 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);
}

// 2018-01-27 更,编者按,,另外,在ES5 中只有一种声明变量的方式,就是使用 var 关键字,然而用它声明的字面量只有在函数中才有作用域概念。然而在 ES6 中,有了 let 关键字,,let 关键字声明的变量没有变量提升,不能在同一个作用域中用let关键字声明同意个变量两次,而且有了明确的作用域概念!!!所以,在上边的例子中,我们可以把 for 循环中的 var 关键字直接改为 let,闭包什么的不需要!完美解决。

记住,在这最后两个例子里,lockedInIndex可以没有任何问题的访问i,但是作为函数的参数使用一个不同的命名标识符可以使概念更加容易的被解释。

立即执行函数一个最显著的优势是就算它没有命名或者说是匿名,函数表达式也可以在没有使用标识符的情况下被立即调用,一个闭包也可以在没有当前变量污染的情况下被使用。

「自执行匿名函数(Self-executing anonymous function)」有什么问题呢?

你看到它已经被提到好几次了,但它仍未被清楚地解释,我提议将术语改成”Immediately-Invoked Function Expression“,或者,IIFE,如果你喜欢缩写的话(发音类似“iffy”)。The pronunciation “iffy“ was suggested to me, and I like it, so let’s go with that.

什么是Immediately-Invoked Function Expression呢?顾名思义,它就是一个被立即调用的函数表达式。Just like the name would lead you to believe.

我想JavaScript社区的成员应该可以在他们的文章里或者陈述里接受术语Immediately-Invoked Function ExpressionIIFE,因为我感觉这样更容易让这个概念被理解,并且self-executing anonymous function真的也的确不够准确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 下面是个自执行函数,递归调用自己本身
function foo(){foo();};

// 这是一个自执行匿名函数。因为它没有标识符,它必须是使用`arguments.callee`(which specifies the currently executing function 特指刚刚执行过的函数)属性来调用它自己
var foo = function(){arguments.callee();};

// 这也许算是一个自执行匿名函数,但是仅仅当`foo`标识符作为它的引用时,如果你将foo换成用其他的什么名字(something else),你会获得一个 "used-to-self-execute" 匿名函数.
var foo = function(){foo();};

// 有些人像这样叫'self-executing anonymous function'下面的函数,即使它不是自执行的,因为它并没有调用它自己。然后,它只是被立即调用了而已。
(function(){ /*code*/ }());

// 为函数表达式增加标识符(也就是说创造一个命名函数)对我们的调试会有很大帮助。一旦命名,函数将不再匿名。
(function foo(){/* code */}());

// IIFEs同样也可以自执行,尽管,也许他不是最有用的模式
(function(){arguments.callee();}())
(function foo(){foo();}())

// 另外,下面这个表达式竟会在黑莓5上抛出错误,在一个被命名的函数中,该函数名是undefined。Awesome, huh?
(function foo(){ foo(); }());

希望上面的例子可以让你更加清楚的知道’self-executing’这个说法是有些误导性的,因为它并不是执行自己的函数,即使函数已经被执行。同样的,匿名属性也没用必要特别指出,因为,Immediately Invoked Function
Expression
,既可以是命名函数也可以匿名函数。

有趣的是:因为arguments.callee在ECMAScript 5 strict mode中被deprecated了,所以在ES5的strict mode中实际上不可能创建一个self-executing anonymous function

最后:模块模式

当我调用函数表达式时,如果我不至少提醒自己一次关于模块模式,我很可能会忽略它。如果你并不熟悉JavaScript里的构建模式,它和我第一个例子很像,但是返回值用对象代替了函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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.get();//0
counter.set(3);
counter.increment();//4
counter.increment();//5

conuter.i;//undefined (`i` is not a property of the returned object)
i;//ReferenceError: i is not defined (it only exists inside the closure)

模块模式方法不仅相当的厉害而且简单。非常少的代码,你可以有效的利用与方法和属性相关的命名,在一个对象里,组织全部的模块代码即最小化了全局变量的污染也创造了私人变量。

延伸阅读

希望这篇文章能够为你提供些有用的信息,并且解答了一些问题。当然,如果你现在比之前的问题更多了,你可以通过阅读以下文章来更深入的了解函数和构建模式。


2018-01-27 更,编者按,,最后希望大家了解下ES6,还是阮一峰老师 => ECMAScript 6 简介

  1. ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov
  2. Functions and function scope - Mozilla Developer Network
  3. Named function expressions - Juriy “kangax” Zaytsev
  4. JavaScript Module Pattern: In-Depth - Ben Cherry
  5. Closures explained with JavaScript - Nick Morgan