Immediately-Invoked Function Expression (IIFE)

让我们开始

在 JavaScript 中,每一个方法被调用时都会创建一个新的 execution context(2)。因为在一个方法中定义的变量和方法只能从内部访问,从外部则不能,而这个调用着方法的 context 就让我们拥有了一个非常简单的途径来实现私有化。

  1. //这个方法将返回一个对‘私有’变量 i 有访问权的方法,这个被返回的方法,形象的说是个‘有特权’的方法。


  2. function makeCounter() {

  3. //‘i’ 只的作用域只在 ‘makeCounter’ 方法内。

  4. var i = 0;


  5. return function() {

  6. console.log( ++i );

  7. };

  8. }


  9. //注意变量 ‘counter’ 和 ‘counter2’ 有各自的变量 ‘i’。


  10. var counter = makeCounter();

  11. counter(); // 输出: 1

  12. counter(); // 输出: 2


  13. var counter2 = makeCounter();

  14. counter2(); // 输出: 1

  15. counter2(); // 输出: 2


  16. i; // ReferenceError: i is not defined (i 只在 makeCounter 方法中存在)

复制代码


在大部分情况下,你不需要拥有这里的 makeCounter 的多个实例,一个对你已经够用了,在其它的一些情况下,你甚至都不会明确的返回一个值。

核心问题

无论你是通过 foo(){} 或 var foo = function(){} 来定义一个方法,都可以通过在其后放一对括号来调用它,就像这样 foo() 。

  1. // 像这样定义的一个方法可以通过在其名字后面加一对()来调用它,如 foo()

  2. // 在此 foo 只是对 function(){/* 一些代码 */} 这个方法的 function expression 的引用


  3. var foo = function(){ /* 一些代码 */ }


  4. // 那么有没有可能直接通过在一个方法表达式后加()来调用它自身呢?


  5. function(){ /* 一些代码 */ }(); // SyntaxError: Unexpected token (

复制代码


就像你看到的,报错了。当解析器遇到一个 function 关键字时,不管是在全局范围或是在另一个方法里,它都会默认的被当作一个 function declaration(3) 而非一个 function expression(4) 来对待。如果你不明确的告诉解析器它正在处理的是一个 expression,它就只会将其当作一个缺少标识符的 function declaration 来对待并因此抛出一个语法错误,因为一个 function declaration 需要一个标识符。

方法,括号和语法错误

如果你给方法指定一个名称,解析器依旧会抛出语法错误,不过这次是全然不同的原因。当()被放置在一个 function declaration 之后时,仅仅会被当作一个分组操作符而已。

  1. // 从语法上来说,这个 function declaration 是有效的,但在其后用()调用是无效的

  2. // 因为这里()仅仅被当作了一个分组操作符,而一个分组操作符中需要包含表达式。


  3. function foo(){ /* 一些代码 */ }(); // SyntaxError: Unexpected token )


  4. // 现在,如果你在()中放一个 expression,错误没有了……

  5. // 不过这个方法依旧不会被执行,因为:


  6. function foo(){ /* 一些代码 */ }( 1 );


  7. // 它还会被视作一个 function declaration 后跟着一个与之全然无关的 expression,等价于这样:


  8. function foo(){ /* 一些代码 */ }


  9. ( 1 );

复制代码


如果你对此想了解更多,请参考 Dmitry A. Soshnikov 的 ECMA-262-3 in detail. Chapter 5. Functions.,里面有更详细的相关内容。

Immediately-Invoked Function Expression (IIFE)

幸运的是,‘修复’语法错误很容易。让解析器‘明确’它正在操作的是一个 function expression 只需要用一个()将它们包裹起来,因为在 JavaScript 里,括号中是不能包含指令的。这样,当解析器遇到 function 关键字时,就会知道将它作为一个 function expression解析,而不是一个 function declaration。

  1. // 以下两种方式都可以实现立即调用一个方法 function expression,并利用方法的 execution context

  2. // 实现‘私有化’


  3. (function(){ /* 一些代码 */ }()); // Crockford 推荐此种方法

  4. (function(){ /* 一些代码 */ })(); // 但是这种方法一样可行


  5. // 因为用括号进行强制操作的目的是消除 function expressions 和方法 function declarations 的歧义,所以

  6. // 如果当解析器已经在之前遇到了一个表达式时也可以省略括号(但请务必往下看)


  7. var i = function(){ return 10; }();

  8. true && function(){ /* 一些代码 */ }();

  9. 0, function(){ /* 一些代码 */ }();


  10. // 如果你不需要返回值,或者想尽可能的让你的代码难读些,你也可以

  11. // 通过一个一元运算符来节省你宝贵的字节


  12. !function(){ /* 一些代码 */ }();

  13. ~function(){ /* 一些代码 */ }();

  14. -function(){ /* 一些代码 */ }();

  15. +function(){ /* 一些代码 */ }();


  16. // 这里还有另一种花样,来自 [url=http://twitter.com/kuvos/status/18209252090847232]@kuvos[/url] ,虽然我搞不懂它为什么行,但它的确行。


  17. new function(){ /* 一些代码 */ }

  18. new function(){ /* 一些代码 */ }() // 只有当需要传递参数时才需要括号

复制代码


关于()的必要性

有时候这些的额外的用来‘消除歧义’的()是多余的(比如解析器在它之前已经遇到了另一个表达式),但作为一个良好惯例我们还是加上它为好。

加上()可以让人知道这个方法会被立即调用,变量指向的是方法的返回结果而非方法本身。这可以方便其他人阅读你的代码,当你的方法很长时,如果不加上(),别人就需要一直滚屏到方法末尾来检查方法是否已经被调用。

明晰的代码不但有必要防止编译器抛出语法错误,也同样有必要防止别人向你抛出“WTFError”(5)

通过闭包保存状态

当我们通过一个方法的标识符调用方法时我们可以对其传递参数,同样的,在使用 IIFE 时我们也可以传递参数。因为这里传入的参数在方法(也就是闭包)中是被‘锁定’的,所以一个 Immediately-Invoked Function Expression 可以有效的被用来保存状态。

如果你想了解闭包的更多知识,请阅读闭包在 JavaScript 中的解释。

  1. // 以下代码不会像你期望的那样工作,因为变量‘i’没有被锁定,每次点击

  2. // 时警示窗都会显示全部的元素数目,因为在那个点上它正是变量‘i’的值


  3. var elems = document.getElementsByTagName( 'a' );


  4. for ( var i = 0; i < elems.length; i++ ) {


  5. elems[ i ].addEventListener( 'click', function(e){

  6. e.preventDefault();

  7. alert( '我是链接 #' + i );

  8. }, 'false' );


  9. }


  10. // 以下的代码可以达到我们的目的,在这个 IIFE 闭包中,变量‘i’像一个

  11. // ‘锁定索引’被锁在其中。当循环结束执行时,尽管变量‘i’的值是元素的总数

  12. // 但在 IIFE 闭包中,‘锁定索引’的值总是当时方法被调用时传入的‘i’值,因此

  13. // 当一个链接被点击时,警示窗就会显示正确的值了。


  14. var elems = document.getElementsByTagName( 'a' );


  15. for ( var i = 0; i < elems.length; i++ ) {


  16. (function( lockedInIndex ){


  17. elems[ i ].addEventListener( 'click', function(e){

  18. e.preventDefault();

  19. alert( '我是链接 #' + lockedInIndex );

  20. }, 'false' );


  21. })( i );


  22. }


  23. // 你也可以像这样使用一个 IIFE,虽然效果相同,但我觉得上面的写法可读性更高。


  24. var elems = document.getElementsByTagName( 'a' );


  25. for ( var i = 0; i < elems.length; i++ ) {


  26. elems[ i ].addEventListener( 'click', (function( lockedInIndex ){

  27. return function(e){

  28. e.preventDefault();

  29. alert( '我是链接 #' + lockedInIndex );

  30. };

  31. })( i ), 'false' );


  32. }

复制代码


注意以上两个实例,虽然这里的‘锁定索引’可以写作 i,没有任何执行问题,但用一个类似方法参数的标识符来代替 i 显然能使代码更具有解释性。

使用 Immediately-Invoked Function Expressions 带来的另一个好处是不会污染当前作用域,因为它是匿名的,没有使用标识符。

“Self-executing anonymous function”有什么问题?

你可能早就听说过这个叫法了,但事实上它并不怎么准确。我认为它应该称作“Immediately-Invoked Function Expression”,或者“IIFE”――――如果你喜欢首字母缩写。有人建议将它发音成“iffy”,我挺喜欢,就这么念好了。

什么是 Immediately-Invoked Function Expression?就是一个被立即调用的方法 expression,就像它的名字告诉我们的。

我希望看到 JavaScript 社区里更多人使用“Immediately-Invoked Function Expression”和“IIFE”的称呼,我觉得这个名称可以更好的诠释这种模式的概念,而“self-executing anonymous function”确实不够准确。

  1. // 这是一个 self-executing function,它执行它自身:


  2. function foo() { foo(); }


  3. // 这是一个 self-executing anonymous function,因为它没有

  4. // 标识符,需要用“arguments.callee”来调用它自身


  5. var foo = function() { arguments.callee(); };


  6. // 这可能是一个 self-executing anonymous function,但只有当“foo”标识符

  7. // 指向它时才是。


  8. var foo = function() { foo(); };


  9. // 一些人把这称作“self-executing anonymous function”哪怕它并没有执行它自身

  10. // 事实上,它是一个立即调用。


  11. (function(){ /* 一些代码 */ }());


  12. // IIFE 可以执行它自身,但大多数情况下这种模式我们用不着。


  13. (function(){ arguments.callee(); }());

  14. (function foo(){ foo(); }());


  15. // 最后一个我们要注意的事情:这样的写法会在 BlackBerry 5(6) 中导致一个错误,因为

  16. // 在一个命名方法中再次出现这个方法的标识符时会被认定为未定义值。


  17. (function foo(){ foo(); }());

复制代码


希望这些例子能帮助大家理解为什么“self-executing”是一种有误导性的说法,因为它并不是方法调用方法自身。同样,匿名也是一个不必要的说词,因为一个立即调用的方法表达式既可以是匿名的也可以是命名的。之所以我用了“调用”而不是“执行”,是因为我认为“IIFE”比“IEFE”看起来更顺眼些,当然读起来也是。

好了,这就是我伟大的想法。

对了,因为在 ECMAScript 5 严格模式下 arguments.callee 已经被弃用,所以技术上来说,在 ECMAScript 5 严谨模式下创造一个“self-executing anonymous function”是不可能的。

关于模块模式

既然都说到了这,那么附带提提模块模式也就成了顺利成章的事。如果你对 JavaScript 的模块模式不熟悉,请回看我的第一个代码示例,只不过用返回一个 Object 来代替返回一个 function(通常像这个例子一样它都是以单例的方式实现的)

  1. // 创建一个立即调用的匿名方法,将它的返回值赋予一个变量。


  2. var counter = (function(){

  3. var i = 0;


  4. return {

  5. get: function(){

  6. return i;

  7. },

  8. set: function( val ){

  9. i = val;

  10. },

  11. increment: function() {

  12. return ++i;

  13. }

  14. };

  15. }());


  16. // ‘counter’ 是一个包含了若干方法的对象


  17. counter.get(); // 0

  18. counter.set( 3 );

  19. counter.increment(); // 4

  20. counter.increment(); // 5


  21. counter.i; // undefined (‘i’ 不包含在返回对象的属性中)

  22. i; // ReferenceError: i is not defined (它只在闭包中存在)

复制代码


模块模式不但强大而且简单。极短的代码就可以让你的方法和属性具有命名空间,最大限度的避免污染全局域并创建私有对象。

延伸阅读

这些文章可以帮助你了解更多更全面的关于方法和模块模式的相关知识。

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



名词注解

(1) 直译的话就是“自身调用自身的方法”或括号中的“自身执行自身的方法”,这个说法与其实质内容有所偏差,故笔者提出了更贴切的 IIFE 的说法。

(2) 通常翻译成执行环境或执行上下文,每一段有不同作用域的 JavaScript 代码都会在一个不同的执行环境中执行。具体中文详解可参见此博客

(3) 可以翻译为方法指令或方法申明,它和 function expression(可译为方法表达式)是 JavaScript 中一对比较容易让人困惑的概念。二者的区别非一言所能详尽,英文好的同学可阅读这片文章

(4) 见注解(3)

(5) 遇到让自己看起来恼火的代码时通常我们会忍不住蹦出“What The Fu...”。

(6) 黑莓5.0系统


你可能感兴趣的:(JavaScript,Web)