立即执行函数(IIFE)

翻译
原文地址

在听到对流行的javascript术语“self-executing anonymous function” (或者self-invoked anonymous function)的许多误导性的理解之后,我决定把我的想法组织成一篇文章。

在本文中,我主要描述了这种模式是如何工作的,以及我们应该如何正确使用这个模式。此外,如果你想快速阅读,你可以只看这些Immediately-Invoked Function Expressions,但是我建议你通过全文。

请注意,本文不是“我对,你错”的东西,我的目的是帮助人们了解复杂的概念。我觉得使用理解一致和准确的术语是人们相互理解的基础。

So, what’s this all about, anyways?

js中每个函数在被调用时,都会创建一个新的执行上下文。函数中定义的变量只能在这个函数的上下文中访问,而不能在函数外访问。因此使用函数能提供一种非常简单的保护隐私的方法。

// makeCounter函数返回了另外一个函数,而这个函数访问了私有变量`i`,因此这个返回函数实际上变成了私有的。

function makeCounter() {
  // i 只能在makeCounter函数内部访问
  var i = 0;

  return function() {
    console.log( ++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未定义(它只存在于makeCounter内部)

The heart of the matter

你定义一个函数,如函数foo(){}var foo = function(){},得到的是一个函数的标识符,可以通过标识符+()的形式调用它,如:foo()

// 可以通过“标识符+()”的方式调用函数,如:foo()。其中foo是一个指向函数表达式 `function() { /* code */ }`的引用

var foo = function(){ /* code */ }

// 那么能不能在函数表达式后面直接添加()来调用它呢?

function(){ /* code */ }(); // SyntaxError: Unexpected token (

正如你所见,有一个catch。 JS解析器在全局作用域函数内部遇到中的function关键字时,默认情况下,将其视为函数声明(语句),而不是函数表达式。 如果你没有明确告诉解析器期望一个表达式,JS解析器认为它看到的是一个没有名字的函数声明,从而抛出一个SyntaxError异常(因为函数声明需要一个名字)。

An aside: functions, parens, and SyntaxErrors

如果你为函数指定名称,并在其后放置括号,则JS解析器也会抛出一个SyntaxError。这个错误是因为:表达式之后的括号表示表达式是要调用的函数,而放在语句之后的括号与前面的语法完全分开,并且只是一个分组运算符(用作控制评估优先级的手段)。

// 虽然这个函数声明现在语法上有效,但它仍然是一个语句, 但是后面的一组括号是无效的,因为
// 分组运算符需要包含一个表达式.

function foo(){ /* code */ }(); // SyntaxError: Unexpected token )

// 如果你在后面的括号中放入表达式,没有异常会抛出,但是这个函数也不会执行,因为下面这个表达式

function foo(){ /* code */ }( 1 );

// 实际上只是等效于这个,一个函数声明后面跟一个完全不相关的表达式,如下:

function foo(){ /* code */ }

( 1 );

你可以在Dmitry A. Soshnikov的文章ECMA-262-3中详细了解这一点。

Immediately-Invoked Function Expression (IIFE)(立即执行函数)

幸运的是,这个SyntaxError很容易“fix”。通过将函数表达式包装在括号中就能解决,因为在JavaScript中,括号不能包含语句,当解析器遇到function关键字时,它知道将其解析为函数表达式而不是函数声明


// 可以使用以下两种模式中的任一种立即调用函数表达式,利用函数的执行上下文来创建“隐私”。

(function(){ /* code */ }()); // Crockford建议采用这一个
(function(){ /* code */ })(); // 同样可以工作


// 因为括号或强制运算符的作用是消除函数表达式和函数声明之间的歧义,当解析器已确定表达式时,
// 可以省略它们(但请参阅下面的“重要注释”)。

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

// 如果你不关心返回值或者代码的可读性,你通过给函数添加一元运算符前缀来保存一个字节

!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

// 这里是另一个变化,我不知道性能的影响,如果有`new`关键字,也可以使用,参考:
// http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
new function(){ /* code */ }() // Only need parens if passing arguments

An important note about those parens

如果不需要围绕函数表达式“消除歧义”的括号(因为解析器已经推导出这是一个表达式),那么在进行赋值时就使用它们是个好主意。这样的括号通常表示函数表达式将被立即调用,变量包含的是函数的结果,而不是函数本身。 这可以节省读取代码的麻烦,否则不得不下滚到可能是一个非常长的函数表达式的底部,以查看它是否已被调用。

Saving state with closures

立即调用函数表达式(IIFE)也能传入参数。因为函数内定义的任何函数可以访问外部函数的传入参数和变量(闭包),所以立即调用的函数表达式(IIFE)可以用于“锁定”值并有效地保存状态。

可以从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' );

}

// 下面这段代码可以执行,因为在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 );

}

// 你也可以像下面这种方式使用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' );

}

立即调用函数表达式(IIFE)的最有利的副作用之一是:匿名的函数表达式被立即调用,所以可以使用闭包而不污染当前范围。

What’s wrong with “Self-executing anonymous function?”

什么是立即调用的函数表达式? 它是一个立即调用的函数表达式。

我建议到JavaScript社区成员在他们的文章和演示文稿中采用术语“立即调用函数表达式”和“IIFE”,因为我认为它使理解这个概念更容易一些,而术语“self-executing anonymous function“不是真的甚至准确。

// 这是一个自执行功能。 它是一个以递归方式执行(或调用)自身的函数:
function foo() { foo(); }

// 这是一个自动执行的匿名函数。 因为它没有标识符,它必须使用`arguments.callee`来执行自身。
var foo = function() { arguments.callee(); };

// 这个可能是一个自动执行的匿名函数,但只有当`foo`标识符实际引用它。如果你把`foo`改成别的东西,
// 你会有一个“used-to-self-execute”的匿名函数。
var foo = function() { foo(); };

// 有些人称之为“自执行匿名函数”,即使它不是自执行的,因为它不会调用自身,但它会立即被调用。
(function(){ /* code */ }());

// 向表达式添加标识符(从而创建一个命名的函数表达式)在调试时非常有用。 
// 然而,一旦命名,该函数不再是匿名的。
(function foo(){ /* code */ }());

// IIFE也可以是自动执行的,尽管这可能不是最有用的模式。
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());

// 最后一点要注意:这将导致BlackBerry 5中的错误,因为在命名的函数表达式中,该名称是未定义的。
(function foo(){ foo(); }());

希望这些例子清楚地表明,“自执行”一词有点误导,因为它不是执行自己的函数,即使函数正在执行。 另外,“匿名”是不必要的特定的,因为立即调用的函数表达式可以是匿名的或命名的。 至于我喜欢“调用”而不是“执行”,这是一个简单的事情; 我认为“IIFE”看起来和听起来比“IEFE”更好。

有趣的事实:因为arguments.callee在ECMAScript 5严格模式中已被弃用,在ECMAScript 5严格模式下创建一个“自执行匿名函数”实际上是不可能的。

A final aside: The Module Pattern

我希望在使用函数表达式时提到模块模式。如果你不熟悉JavaScript中的模块模式,它类似于我的第一个例子,但是返回一个Object而不是一个函数(并且通常实现为一个单例,如在这个例子中)。

// 创建一个立即调用的匿名函数表达式,并将其返回值赋给一个变量。 这种方法“切断了中间人”
// 命名的`makeWhatever`函数引用。正如上面的“重要说明”中所解释,尽管在这个函数表
// 达式周围不需要括号,但它们仍然应该作为约定使用,以帮助阐明变量被设置为函数的result
// 而不是函数本身。

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; // ReferenceError: i 未定义 (只在闭包中存在)

模块模式方法既强大又简单。使用非常少的代码,您可以有效地命名空间相关的方法和属性,以最小化全局范围污染和创建隐私的方式组织整个代码模块。

Further reading
希望这篇文章能回答了你的一些问题。 当然,如果你现在有更多的问题,你可以通过阅读下面的文章更多地了解函数和模块模式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容