JS 立即调用函数表达式 IIFE

写在前面

  • 因为最近在学习模块化编程,而模块化编程,就是为了解决污染全局变量的问题。我想应该都是通过立即调用函数表达式实现的吧。
  • 这是一篇译文,原文: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)
  • 模块模式的方法不仅非常强大,而且非常简单。使用非常少的代码,你就可以有效的命名空间相关的方法和属性,以最小化全局污染和保持私有的方式组织整个代码模块。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,148评论 0 13
  • 星期六9月30日,我们在教室里做了月饼,接下来,我来给你们说一说,我们是怎样做月饼的吧! 到了下午...
    李如淼阅读 182评论 0 0
  • 我是个简单而无趣的人,这个当然是我自我评价,周遭很多朋友或者同事都说每天看到我嘻嘻哈哈,说的话聊的天都是趣味...
    小小家阅读 583评论 0 0
  • 文/梅子花开 写下这个话题,不是说我多么有婆媳相处的经验技巧,也不是说我和婆婆相处的多么融洽。对于别人的婆媳关系,...
    梅子花开心语阅读 485评论 0 0
  • 最近因为柯希莫,经常想起胖胖金先生。 多年前养过的他,好像没被好好对待过。很多时候被当作一个玩伴,缺少正确的关心和...
    connie34阅读 201评论 2 0